# Venon Public API: full reference > Every endpoint with its required scope, parameters, request body and response fields, generated from the OpenAPI specification as one Markdown file for AI agents and tools that cannot run the interactive reference. - REST base URL: https://developer-next.venon.io - MCP server (Streamable HTTP, same API key or OAuth bearer token): https://developer-next.venon.io/mcp - OpenAPI 3.1 specification with complete JSON schemas: https://developerdocs.venon.io/openapi.json - Shipping profiles guide: https://developerdocs.venon.io/shipping-profiles.md The Venon Public API provides programmatic access to your advertising analytics and profit-tracking cost data. ## Authentication All endpoints (except /v1/health) require API key authentication. You can authenticate using either: 1. **Bearer Token** (recommended): ``` Authorization: Bearer vnon_xxx... ``` 2. **X-API-Key Header**: ``` X-API-Key: vnon_xxx... ``` API keys are created and managed through the Venon Dashboard. ## Rate Limiting Limits apply per credential: **60 requests per minute** per API key for REST (ads write endpoints: **10 per minute**), and 60 per minute per API key or OAuth token for MCP. Requests without credentials, and repeated failed authentication, are limited per IP address. Responses include `RateLimit-Policy`, `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the window resets). When rate limited, you'll receive a 429 response with a `Retry-After` header (seconds). ## Scopes API keys have specific scopes that control access: - `analytics:read` - Read analytics, product performance, attribution, and insight endpoints - `orders:read` - Read orders and per-order cost breakdowns - `cost:read` - Read profit-tracking cost settings: COGS, shipping, and payment fees - `cost:write` - Create and update profit-tracking cost settings - `ads:write` - Activate/pause campaigns, ad sets, and ads, and update campaign / ad-set daily budgets (Meta + Google) - `filters:read` - Read saved data filters (applying them to a report needs only that report's scope) - `filters:write` - Create, update and delete saved data filters ## Response Format All responses follow a consistent structure: **Success:** ```json { "success": true, "data": { ... }, "metadata": { "request_id": "req_xxx", "timestamp": "2025-04-15T12:00:00.000Z" } } ``` **Error:** ```json { "success": false, "error": { "code": "ERROR_CODE", "message": "Human-readable message", "details": [{ "field": "...", "message": "..." }] }, "metadata": { ... } } ``` ## Choosing an Endpoint Use the shop summary for headline profit, timeseries for trends, and profit-loss for a statement. Use channel/campaign/ad performance for attributed marketing results, and attribution drill-downs for the matching orders or products. Use the product catalog to resolve identities before a product/variant cost write; product performance only answers sales questions for a date window. Cost writes configure Venon's profit calculations. Ads writes change the connected ad platform immediately. A read-before-write workflow needs the relevant read scope as well as the write scope. ## Dates, Pagination and IDs Date parameters use shop-local calendar dates; keep dates, attribution settings and filters consistent when comparing reports. Follow each endpoint's range and result limits. Cohort follow-up depth and report breakdown limits are separate from pagination. Endpoints exposing `page`/`per_page` use 1-based pages (default 50 items, maximum 100 per page). Read the returned pagination metadata and keep filters unchanged between pages. COGS rules instead use `limit`/`offset`; shipping profiles and payment-gateway lists do not use page pagination. Totals covering the full filtered set must not be added again for each page. Use IDs returned by the corresponding read operation, preserving the schema's string/number type. Display names, SKUs, product IDs, cost-history row IDs and ad-platform IDs are not interchangeable. Resolve ambiguous matches before writing. ## Interpreting Results `metadata.timestamp` is the response creation time, not the last data-sync time. Use report-specific `data_updated_at` and availability fields where provided. Preserve null metrics when a value is unavailable or its denominator is zero. Ratios and cumulative totals are not additive. A successful bulk cost response can contain failed targets; inspect its `applied` and `failed` fields. A dry-run plan performs no write and does not reserve state. Keep `metadata.request_id` when reporting an error; on a REST 429, wait for `Retry-After` before retrying. ## MCP Conventions Use each tool's input schema: REST body wrappers are not automatically MCP arguments. For example, analytics multi-ID filters are comma-separated strings in REST and arrays of strings in MCP. Successful MCP results expose `structuredContent.data` while retaining the existing text result; handler business errors use `isError: true` and `structuredContent.error`. Inspect the error rather than treating the MCP transport's HTTP success as a successful operation. ## Attribution Models - `linear_paid` - Credit distributed evenly across paid touchpoints - `linear_all` - Credit distributed evenly across all touchpoints - `first_click` - All credit to first touchpoint - `last_click` - All credit to last touchpoint - `last_paid_click` - All credit to last paid touchpoint - `all_clicks` - Each touchpoint gets full credit, so channel totals can exceed revenue ## Attribution Windows - `1_day`, `7_day`, `14_day`, `28_day`, `90_day`, `lifetime` When omitted, `attribution_model` defaults to `last_paid_click` and `attribution_window` to `lifetime` on every endpoint and MCP tool. The dashboard opens on all_clicks with a lifetime window and remembers each user's choice; pass the model and window shown there to reproduce its numbers. ## Endpoint index ### System - `GET /v1/health`: Health check (no authentication) ### Analytics - `GET /v1/analytics/ad-sets`: Get ad set performance metrics (`analytics:read`) - `GET /v1/analytics/ads`: Get ad performance metrics (`analytics:read`) - `GET /v1/analytics/campaigns`: Get campaign performance metrics (`analytics:read`) - `GET /v1/analytics/channels`: Get channel performance metrics (`analytics:read`) - `GET /v1/analytics/cohorts`: Get cohort analysis (`analytics:read`) - `GET /v1/analytics/product-relationships`: Get product relationships (`analytics:read`) - `GET /v1/analytics/profit-loss`: Get profit & loss statement (`analytics:read`) - `GET /v1/analytics/retention`: Get retention report (`analytics:read`) - `GET /v1/analytics/summary`: Get shop P&L summary (`analytics:read`) - `GET /v1/analytics/timeseries`: Get core-metric timeseries (`analytics:read`) ### Orders - `GET /v1/orders`: List orders (`orders:read`) - `GET /v1/orders/{id}`: Get an order cost breakdown (`orders:read`) ### Products - `GET /v1/products`: Get product performance (`analytics:read`) - `GET /v1/products/catalog`: Look up the product catalog (`analytics:read`) ### Attribution - `GET /v1/attribution/campaigns`: Get non-ad channel campaign performance (`analytics:read`) - `GET /v1/attribution/orders`: List attributed orders (`analytics:read` + `orders:read`) - `GET /v1/attribution/products`: List attributed products (`analytics:read`) ### Cost - `POST /v1/cost/cogs/bulk`: Bulk-set product COGS (`cost:write`) - `GET /v1/cost/cogs/global`: Get global COGS settings (`cost:read`) - `PUT /v1/cost/cogs/global`: Update global COGS settings (`cost:write`) - `GET /v1/cost/cogs/rules`: List COGS rules (`cost:read`) - `POST /v1/cost/cogs/rules`: Create a COGS rule (`cost:write`) - `POST /v1/cost/cogs/rules/bulk`: Set different COGS rules in one batch (`cost:write`) - `POST /v1/cost/cogs/rules/bulk-read`: Read selected COGS rule histories in bulk (`cost:read`) - `POST /v1/cost/cogs/rules/history`: Set the full COGS history timeline (`cost:write`) - `POST /v1/cost/cogs/rules/ops`: Apply COGS history operations (`cost:write`) - `GET /v1/cost/cogs/rules/{id}`: Get a COGS rule (`cost:read`) - `GET /v1/cost/payment-gateways`: List payment gateways (`cost:read`) - `POST /v1/cost/payment-gateways`: Upsert a payment gateway (`cost:write`) - `POST /v1/cost/payment-gateways/bulk`: Bulk-set payment fees (`cost:write`) - `POST /v1/cost/payment-gateways/history`: Set the full payment gateway fee timeline (`cost:write`) - `POST /v1/cost/payment-gateways/ops`: Apply payment gateway history operations (`cost:write`) - `GET /v1/cost/shipping-methods`: List shipping methods (`cost:read`) - `GET /v1/cost/shipping-profiles`: List shipping profiles (`cost:read`) - `POST /v1/cost/shipping-profiles`: Upsert a shipping profile (`cost:write`) - `POST /v1/cost/shipping-profiles/bucket-history`: Set the full cost timeline for one shipping bucket (`cost:write`) - `POST /v1/cost/shipping-profiles/fee-history`: Set the full timeline for one scalar shipping fee (`cost:write`) - `POST /v1/cost/shipping-profiles/tiers`: Set weight/item shipping tiers (`cost:write`) - `GET /v1/cost/shipping-profiles/{id}`: Get a shipping profile (`cost:read`) - `DELETE /v1/cost/shipping-profiles/{id}`: Delete a shipping profile (`cost:write`) - `POST /v1/cost/shipping-profiles/{id}/cost-updates`: Update multiple tier costs from an effective date, preserving history (`cost:write`) ### Ads - `PATCH /v1/ads/{channel}/ad-sets/{ad_set_id}/budget`: Update an ad set daily budget (meta-ads only) (`ads:write`) - `PATCH /v1/ads/{channel}/ad-sets/{ad_set_id}/status`: Activate or pause an ad set (`ads:write`) - `PATCH /v1/ads/{channel}/ads/{ad_id}/status`: Activate or pause an ad (`ads:write`) - `PATCH /v1/ads/{channel}/campaigns/{campaign_id}/budget`: Update a campaign daily budget (`ads:write`) - `PATCH /v1/ads/{channel}/campaigns/{campaign_id}/status`: Activate or pause a campaign (`ads:write`) ### Data Filters - `GET /v1/data-filters`: List saved data filters (`filters:read`) - `POST /v1/data-filters`: Create a saved data filter (`filters:write`) - `PUT /v1/data-filters/{id}`: Replace a saved data filter (`filters:write`) - `DELETE /v1/data-filters/{id}`: Delete a saved data filter (`filters:write`) ## Endpoints ### System System and health endpoints #### GET /v1/health Health check Returns the health status of the Public API. **Authentication:** Not required This endpoint can be used for monitoring and uptime checks. A successful health check confirms this API route responds; it does not validate your API key, connected integrations, or the freshness of analytics data. Response (success envelope `data`): - `data`: HealthResponseData - `status`: "ok" - `version`: string - `service`: string ### Analytics Advertising analytics, performance metrics, and shop insights (P&L, timeseries, cohorts, retention) #### GET /v1/analytics/ad-sets Get ad set performance metrics Returns performance metrics for ad sets within a date range. **Two modes:** - **By-ID** — supply `ad_set_ids` (and `channel`) to get metrics for specific ad sets. - **List** — omit `ad_set_ids` to list every ad set with spend or attributed activity in the window. Filter by `channel` (omit for all ad-spend channels), `min_spend`/`max_spend`, `active_in_range`, `status`, and `q`; sort by `sort_by`/`order`; paginate with `page`/`per_page`. List items additionally carry the parent `campaign_id`, `active`, and `budget`; the response includes a `pagination` block and `totals` over the full filtered set. **Note:** list mode is join-driven over the window — it returns every ad set with **either** ad spend **or** attributed activity (orders/revenue) in the window. Because attribution is keyed on order date and spend on spend date, an ad set can appear with `ad_spend = 0` when it converted in-window from spend that landed outside it; pass `active_in_range=true` to keep only ad sets with spend > 0. Google Ads has no ad-set level, so `/ad-sets` is empty for `google-ads`. **Required scope:** `analytics:read` **Rate limit:** 60 requests per minute per API key Dates are inclusive shop-local calendar days (YYYY-MM-DD), with start_date <= end_date and at most 366 days per request. Keep the same date range when comparing reports. Keep attribution_model and attribution_window identical when comparing channels or drilling down into campaigns, orders and products; changing them changes the credited results. List-mode status is the stored lifecycle state, not historical delivery; active_in_range filters for spend in the requested window. Use pagination to fetch further pages; totals cover the full filtered set, so do not add them again for each page. filter_mode applies saved data filters to this report: none (default) applies none, enabled applies the filters currently enabled in the dashboard, selected applies exactly filter_ids (enabled or not) without changing their saved state. filter_ids is required with selected and rejected otherwise; an unknown id fails the request (404), and so does a selected filter whose saved rules are no longer valid (409). Enabled mode skips such a filter and still answers, with a warning (metadata.warnings over REST, warnings over MCP). Shopify filters narrow store orders. An ad channel filter removes the excluded spend and attributed credit of that channel only; the credit is not re-attributed and store orders stay unchanged. Several filters all apply (AND). Response metadata.data_filters names the applied filters. Query parameters: - `ad_set_ids` (string): Comma-separated platform ad set IDs (max 100). Omit for list mode. - `channel` ("meta-ads" | "google-ads" | "taboola" | "tiktok-ads" | "outbrain" | "microsoft-ads" | "pinterest-ads"): Ad channel identifier. Omit to span all ad-spend channels. - `start_date` (string, required): Start date (YYYY-MM-DD) - `end_date` (string, required): End date (YYYY-MM-DD) - `attribution_model` ("linear_paid" | "linear_all" | "first_click" | "last_click" | "last_paid_click" | "all_clicks", default "last_paid_click"): Attribution model - `attribution_window` ("1_day" | "7_day" | "14_day" | "28_day" | "90_day" | "lifetime", default "lifetime"): Attribution window - `filter_mode` ("none" | "enabled" | "selected"): Saved data filters to apply: none (default), enabled or selected - `filter_ids` (string): Comma-separated saved data filter ids (max 20); only with filter_mode=selected - `include` ("creative_metrics"): creative_metrics adds Meta creative metrics to meta-ads items. - `min_spend` (number | null): Filter: minimum ad_spend in the window (inclusive) - `max_spend` (number | null): Filter: maximum ad_spend in the window (inclusive) - `active_in_range` ("true" | "false"): Filter: keep only entities with ad_spend > 0 in the window ("active in time range") - `status` ("active" | "paused" | "all", default "all"): Filter on the platform lifecycle flag (time-independent). Default `all`. - `q` (string): Case-insensitive substring match on the entity name - `sort_by` ("ad_spend" | "name" | "attributed_revenue" | "roas" | "attributed_orders", default "ad_spend"): List-mode sort field. Default `ad_spend`. - `order` ("asc" | "desc", default "desc"): List-mode sort order. Default `desc`. - `page` (integer, default 1): List-mode page number. Default 1. - `per_page` (integer, default 50): List-mode items per page (max 100). Default 50. Response (success envelope `data`): - `data`: AdSetPerformanceResponseData - `ad_sets`: AdSetPerformanceData[] - `ad_set_id`: string - `ad_set_name`: string | null - `channel`: string - `campaign_id`: string - `active`: boolean - `budget`: number | null - `metrics`: AdMetrics - `ad_spend`: number - `attributed_orders`: number - `attributed_revenue`: number - `roas`: number - `net_profit`: number - `first_time_customer_orders`: number - `first_time_customer_costs`: number - `first_time_customer_revenue`: number - `first_time_customer_roas`: number - `impressions`: number - `clicks`: number - `platform_conversions`: number - `platform_conversion_value`: number - `creative_metrics`: object - `totals`: PerformanceTotals - `impressions`: number - `clicks`: number - `ad_spend`: number - `attributed_orders`: number - `first_time_customer_orders`: number - `first_time_customer_costs`: number - `attributed_revenue`: number - `roas`: number - `pagination`: PaginationMetadata - `page`: number - `per_page`: number - `total_items`: number - `total_pages`: number - `has_next`: boolean - `has_previous`: boolean #### GET /v1/analytics/ads Get ad performance metrics Returns performance metrics for ads (creative-level) within a date range. **Two modes:** - **By-ID** — supply `ad_ids` (and `channel`) to get metrics for specific ads. - **List** — omit `ad_ids` to list every ad with spend or attributed activity in the window. Filter by `channel` (omit for all ad-spend channels), `min_spend`/`max_spend`, `active_in_range`, `status`, and `q`; sort by `sort_by`/`order`; paginate with `page`/`per_page`. List items additionally carry the parent `campaign_id` and `ad_set_id` plus `active`; the response includes a `pagination` block and `totals` over the full filtered set. **Note:** list mode is join-driven over the window — it returns every ad with **either** ad spend **or** attributed activity (orders/revenue) in the window. Because attribution is keyed on order date and spend on spend date, an ad can appear with `ad_spend = 0` when it converted in-window from spend that landed outside it; pass `active_in_range=true` to keep only ads with spend > 0. **Required scope:** `analytics:read` **Rate limit:** 60 requests per minute per API key (Same note as under `GET /v1/analytics/ad-sets`.) Query parameters: - `ad_ids` (string): Comma-separated platform ad IDs (max 100). Omit for list mode. - `channel`: as under `GET /v1/analytics/ad-sets` - `start_date` (required): as under `GET /v1/analytics/ad-sets` - `end_date` (required): as under `GET /v1/analytics/ad-sets` - `attribution_model`: as under `GET /v1/analytics/ad-sets` - `attribution_window`: as under `GET /v1/analytics/ad-sets` - `filter_mode`: as under `GET /v1/analytics/ad-sets` - `filter_ids`: as under `GET /v1/analytics/ad-sets` - `include`: as under `GET /v1/analytics/ad-sets` - `min_spend`: as under `GET /v1/analytics/ad-sets` - `max_spend`: as under `GET /v1/analytics/ad-sets` - `active_in_range`: as under `GET /v1/analytics/ad-sets` - `status`: as under `GET /v1/analytics/ad-sets` - `q`: as under `GET /v1/analytics/ad-sets` - `sort_by`: as under `GET /v1/analytics/ad-sets` - `order`: as under `GET /v1/analytics/ad-sets` - `page`: as under `GET /v1/analytics/ad-sets` - `per_page`: as under `GET /v1/analytics/ad-sets` Response (success envelope `data`): - `data`: AdPerformanceResponseData - `ads`: AdPerformanceData[] - `ad_id`: string - `ad_name`: string | null - `channel`: string - `campaign_id`: string - `ad_set_id`: string - `active`: boolean - `metrics`: AdMetrics (fields as under `GET /v1/analytics/ad-sets`) - `totals`: PerformanceTotals (fields as under `GET /v1/analytics/ad-sets`) - `pagination`: PaginationMetadata (fields as under `GET /v1/analytics/ad-sets`) #### GET /v1/analytics/campaigns Get campaign performance metrics Returns performance metrics for campaigns within a date range. **Two modes:** - **By-ID** — supply `campaign_ids` (and `channel`) to get metrics for specific campaigns. - **List** — omit `campaign_ids` to list every campaign with spend or attributed activity in the window. Filter by `channel` (omit for all ad-spend channels), `min_spend`/`max_spend`, `active_in_range`, `status`, and `q`; sort by `sort_by`/`order`; paginate with `page`/`per_page`. List items additionally carry `active` and `budget`; the response includes a `pagination` block and `totals` over the full filtered set. **Note:** list mode is join-driven over the window — it returns every campaign with **either** ad spend **or** attributed activity (orders/revenue) in the window. Because attribution is keyed on order date and spend on spend date, a campaign can appear with `ad_spend = 0` when it converted in-window from spend that landed outside it; pass `active_in_range=true` to keep only campaigns with spend > 0. **Required scope:** `analytics:read` **Rate limit:** 60 requests per minute per API key (Same note as under `GET /v1/analytics/ad-sets`.) Query parameters: - `campaign_ids` (string): Comma-separated platform ad campaign IDs (max 100). Omit for list mode. - `channel`: as under `GET /v1/analytics/ad-sets` - `start_date` (required): as under `GET /v1/analytics/ad-sets` - `end_date` (required): as under `GET /v1/analytics/ad-sets` - `attribution_model`: as under `GET /v1/analytics/ad-sets` - `attribution_window`: as under `GET /v1/analytics/ad-sets` - `filter_mode`: as under `GET /v1/analytics/ad-sets` - `filter_ids`: as under `GET /v1/analytics/ad-sets` - `include`: as under `GET /v1/analytics/ad-sets` - `min_spend`: as under `GET /v1/analytics/ad-sets` - `max_spend`: as under `GET /v1/analytics/ad-sets` - `active_in_range`: as under `GET /v1/analytics/ad-sets` - `status`: as under `GET /v1/analytics/ad-sets` - `q`: as under `GET /v1/analytics/ad-sets` - `sort_by`: as under `GET /v1/analytics/ad-sets` - `order`: as under `GET /v1/analytics/ad-sets` - `page`: as under `GET /v1/analytics/ad-sets` - `per_page`: as under `GET /v1/analytics/ad-sets` Response (success envelope `data`): - `data`: CampaignPerformanceResponseData - `campaigns`: CampaignPerformanceData[] - `campaign_id`: string - `campaign_name`: string | null - `channel`: string - `active`: boolean - `budget`: number | null - `metrics`: AdMetrics (fields as under `GET /v1/analytics/ad-sets`) - `totals`: PerformanceTotals (fields as under `GET /v1/analytics/ad-sets`) - `pagination`: PaginationMetadata (fields as under `GET /v1/analytics/ad-sets`) #### GET /v1/analytics/channels Get channel performance metrics Returns performance metrics for all or specified channels within a date range. **Required scope:** `analytics:read` **Rate limit:** 60 requests per minute per API key Dates are inclusive shop-local calendar days (YYYY-MM-DD), with start_date <= end_date and at most 366 days per request. Keep the same date range when comparing reports. Keep attribution_model and attribution_window identical when comparing channels or drilling down into campaigns, orders and products; changing them changes the credited results. Use this report for attributed channel comparisons and the shop summary for blended shop-wide performance. Channel totals depend on the chosen attribution model. Channels include paid ad channels (meta-ads, google-ads, …) and non-paid ones: organic (dashboard "Search / Social Media"), direct ("Direct Traffic"), email such as klaviyo, and others; non-paid channels report ad_spend 0 and ROAS 0. (Same note as under `GET /v1/analytics/ad-sets`.) Query parameters: - `channels` (string): Comma-separated channel identifiers — paid (meta-ads, google-ads, …) or non-paid (organic, direct, klaviyo, …). Omit for all channels. - `start_date` (required): as under `GET /v1/analytics/ad-sets` - `end_date` (required): as under `GET /v1/analytics/ad-sets` - `attribution_model`: as under `GET /v1/analytics/ad-sets` - `attribution_window`: as under `GET /v1/analytics/ad-sets` - `filter_mode`: as under `GET /v1/analytics/ad-sets` - `filter_ids`: as under `GET /v1/analytics/ad-sets` Response (success envelope `data`): - `data`: ChannelPerformanceResponseData - `channels`: ChannelPerformanceData[] - `channel`: string - `metrics`: ChannelMetrics - `ad_spend`: number - `attributed_orders`: number - `attributed_revenue`: number - `roas`: number - `net_profit`: number - `first_time_customer_orders`: number - `first_time_customer_costs`: number - `first_time_customer_revenue`: number - `first_time_customer_roas`: number - `totals`: ChannelTotals - `ad_spend`: number - `attributed_orders`: number - `first_time_customer_orders`: number - `first_time_customer_costs`: number - `attributed_revenue`: number - `roas`: number #### GET /v1/analytics/cohorts Get cohort analysis Cohort retention matrix: customers grouped by acquisition week/month/quarter/year, with per-period incremental and cumulative metrics — active customers, retention %, orders, net revenue, CM1/CM3, AOV, LTV-to-date, LTV:CAC ratios, and payback flag — plus cohort size, ad spend, and CAC per cohort. Optionally filter to customers whose first order contained a specific product/variant. **Required scope:** `analytics:read` **Rate limit:** 60 requests per minute per API key The date window selects acquisition cohorts. max_periods controls follow-up depth, not pagination. Compare cohorts at the same elapsed period; a recent cohort has less observed history. Cumulative metrics already include earlier periods and must not be summed. Query parameters: - `start_date` (string, required): First cohort start date (YYYY-MM-DD) - `end_date` (string): Last cohort end date (YYYY-MM-DD). Defaults to today. - `cohort_type` ("week" | "month" | "quarter" | "year", required): Cohort bucketing period - `max_periods` (integer): Retention periods per cohort (defaults per cohort_type) - `filter_product_id` (integer | null): Only cohorts whose first order contained this product - `filter_variant_id` (integer | null): Only cohorts whose first order contained this variant Response (success envelope `data`): - `data`: CohortsResponseData - `cohorts`: CohortData[] - `cohort`: string - `cohort_size`: number - `cohort_ad_spend`: number - `cac_per_customer`: number - `periods`: object[] - `period`: number - `metrics`: object - `cohort_type`: string - `max_periods`: number - `currency`: string #### GET /v1/analytics/product-relationships Get product relationships Cross-sell and repurchase relationships between products: which products are bought together in one order, what customers buy next, and per-product stats (customers, orders, units, repurchase rate, first-order vs repeat-order share) with related-product breakdowns. The underlying data is rebuilt daily — see `data_updated_at`. **Required scope:** `analytics:read` **Rate limit:** 60 requests per minute per API key product_limit, related_limit and top_limit cap different result lists; these are not page sizes for traversing the complete catalog. Check data_updated_at for freshness. These relationships describe observed purchases and do not establish that one product caused another sale. Query parameters: - `start_date` (string, required): Start date (YYYY-MM-DD, shop-local) - `end_date` (string): End date (YYYY-MM-DD). Defaults to today. - `product_limit` (integer): Anchor products returned, highest customer count first - `related_limit` (integer): Related products per anchor - `top_limit` (integer): Rows in the top-bought-together / top-buy-next lists Response (success envelope `data`): - `data`: ProductRelationshipsResponseData - `top_bought_together`: object[] - `product_a_id`: string - `product_a`: string - `product_b_id`: string - `product_b`: string - `order_count`: number - `pair_rate`: number - `top_buy_next`: object[] - `first_product_id`: string - `first_product`: string - `next_product_id`: string - `next_product`: string - `customer_count`: number - `next_rate`: number - `products`: object[] - `product_id`: string - `product_name`: string - `image_url`: string - `customers`: number - `orders`: number - `total_units_sold`: number - `avg_units_per_customer`: number - `repurchase_rate`: number - `first_order_rate`: number - `repeat_order_rate`: number - `related_products`: object[] - `product_id`: string - `product_name`: string - `same_order_count`: number - `same_order_rate`: number - `first_order_together_count`: number - `first_order_together_rate`: number - `bought_directly_after_count`: number - `bought_directly_after_rate`: number - `bought_after_count`: number - `bought_after_rate`: number - `same_customer_count`: number - `same_customer_rate`: number - `data_updated_at`: string | null - `currency`: string #### GET /v1/analytics/profit-loss Get profit & loss statement Profit & loss statement for a date window as ordered rows (gross revenue, deductions, and subtotals down to net profit — each with its value and share of gross revenue) plus waterfall-chart steps. This is the same P&L statement the merchant sees on the dashboard. **Required scope:** `analytics:read` **Rate limit:** 60 requests per minute per API key Dates are inclusive shop-local calendar days (YYYY-MM-DD), with start_date <= end_date and at most 366 days per request. Keep the same date range when comparing reports. Use the ordered rows for a statement or waterfall. Subtotal rows already include earlier components; summing every row would count the same amounts more than once. Query parameters: - `start_date` (required): as under `GET /v1/analytics/product-relationships` - `end_date` (string, required): End date (YYYY-MM-DD, shop-local) Response (success envelope `data`): - `data`: ProfitLossResponseData - `rows`: ProfitLossRow[] - `key`: string - `label`: string - `kind`: "currency" | "number" - `emphasis`: "subtotal" - `muted`: boolean - `inverted`: boolean - `value`: number | null - `percent`: number | null - `waterfall`: object | null - `steps`: ProfitLossWaterfallStep[] - `key`: string - `label`: string - `type`: "subtotal" | "cost" - `value`: number - `from`: number - `to`: number - `pct`: number | null - `max`: number #### GET /v1/analytics/retention Get retention report Retention report for customers acquired in the window: CLV, repeat-purchase rate, days to second purchase, CLV:CAC, new vs returning AOV, order-gap distribution, expected-next-order timing (winback / at-risk / lost thresholds), channel-level retention, repurchase windows, LTV by first product, and period LTV (90/180/365 days). `metadata.availability` flags sections with insufficient data. **Required scope:** `analytics:read` **Rate limit:** 60 requests per minute per API key Check the report metadata.availability, including reasons and sample sizes, before drawing conclusions. Missing or null metrics are not evidence of zero retention. Keep refund_attribution consistent across comparisons; product_limit and channel_limit cap breakdown sizes, not pages. Query parameters: - `start_date` (required): as under `GET /v1/analytics/product-relationships` - `end_date` (required): as under `GET /v1/analytics/profit-loss` - `refund_attribution` ("refund_date" | "order_date" | "ignore_refunds", default "refund_date"): How refunds are assigned to periods - `product_limit` (integer, default 50): Max products in the LTV-by-first-product section - `channel_limit` (integer, default 20): Max channels in the channel breakdown Response (success envelope `data`): - `data`: RetentionResponseData - `metadata`: object - `start_date`: string - `end_date`: string - `currency`: string - `timezone`: string - `refund_attribution`: "refund_date" | "order_date" | "ignore_refunds" - `data_updated_at`: string | null - `availability`: object - `limits`: object - `product_limit`: number - `channel_limit`: number - `kpis`: object - `avg_clv`: number | null - `pct_repeat_purchasers`: number | null - `avg_days_to_second_purchase`: number | null - `clv_to_cac_ratio`: number | null - `avg_days_between_orders`: number | null - `new_customer_aov`: number | null - `returning_customer_aov`: number | null - `avg_orders_per_customer`: number | null - `avg_products_per_order`: number | null - `avg_contribution_profit`: number | null - `avg_net_profit`: number | null - `contribution_margin_pct`: number | null - `avg_cm_per_order`: number | null - `paid_new_customer_cac`: number | null - `clv_cac_trend`: object[] - `period_label`: string - `clv`: number | null - `cac`: number | null - `clv_to_cac`: number | null - `expected_next_order`: object | null - `avg_days_to_second_purchase`: number - `avg_days_between_orders`: number - `winback_start_days`: number - `at_risk_days`: number - `lost_days`: number - `sample_size`: number - `method`: string - `aov_timeseries`: object[] - `date`: string - `new_aov`: number | null - `returning_aov`: number | null - `order_gap_distribution`: object[] - `from_order`: number - `to_order`: number - `avg_days`: number - `sample_size`: number - `channel_breakdown`: object[] - `channel`: string - `customer_count`: number - `new_customer_aov`: number | null - `repeat_rate`: number | null - `avg_days_to_second_purchase`: number | null - `paid_new_customer_cac`: number | null - `cm3_per_customer`: number | null - `is_paid`: boolean | null - `repurchase_windows`: object[] - `window_days`: number - `pct_customers`: number | null - `mature_customer_count`: number - `repeat_customer_count`: number - `ltv_by_first_product`: object[] - `product_id`: string - `product_name`: string - `image_url`: string - `first_purchase_customers`: number - `ltv_90d`: number | null - `ltv_180d`: number | null - `ltv_365d`: number | null - `repeat_rate`: number | null - `net_ltv_90d`: number | null - `net_ltv_180d`: number | null - `net_ltv_365d`: number | null - `contribution_margin_pct`: number | null - `mature_customers`: object - `days_90`: number - `days_180`: number - `days_365`: number - `period_ltv`: object[] - `window_days`: number - `ltv`: number | null - `cumulative_revenue`: number | null - `profit_ltv`: number | null - `cumulative_profit`: number | null - `mature_customer_count`: number #### GET /v1/analytics/summary Get shop P&L summary Shop-wide P&L summary for a date window: orders, gross/net revenue, refunds, tax, COGS, payment fees, shipping fees, ad spend, the contribution-margin ladder (CM1 = net revenue − COGS; CM2 = CM1 − payment − shipping; CM3 = CM2 − ad spend), blended ROAS/MER (gross revenue ÷ ad spend), POAS (CM2 ÷ ad spend), AOV, new customers, new-customer revenue, and CAC. Figures are shop-wide absolutes — no attribution model applies. **Required scope:** `analytics:read` **Rate limit:** 60 requests per minute per API key Dates are inclusive shop-local calendar days (YYYY-MM-DD), with start_date <= end_date and at most 366 days per request. Keep the same date range when comparing reports. Use this single aggregate for a headline shop report. Null ratios mean the denominator does not support a value; preserve null instead of displaying zero performance. (Same note as under `GET /v1/analytics/ad-sets`.) Query parameters: - `start_date` (required): as under `GET /v1/analytics/product-relationships` - `end_date` (required): as under `GET /v1/analytics/profit-loss` - `filter_mode`: as under `GET /v1/analytics/ad-sets` - `filter_ids`: as under `GET /v1/analytics/ad-sets` Response (success envelope `data`): - `data`: AnalyticsSummaryData - `orders`: number - `gross_revenue`: number - `refunds`: number - `net_revenue`: number - `tax`: number - `cogs`: number - `payment_fees`: number - `shipping_fees`: number - `ad_spend`: number - `cm1`: number - `cm2`: number - `cm3`: number - `roas`: number | null - `mer`: number | null - `poas`: number | null - `aov`: number | null - `new_customers`: number - `new_customer_revenue`: number - `cac`: number | null #### GET /v1/analytics/timeseries Get core-metric timeseries Timeseries of core shop metrics per bucket: orders, gross revenue, refunds, COGS, ad spend, payment/shipping fees, tax, net revenue, CM1/CM2/profit (CM3), blended ROAS, new customers, new-customer revenue/ROAS, and CAC. `granularity=auto` (default) returns hourly buckets for very short ranges and daily otherwise; `daily` forces day buckets; `weekly` re-buckets to ISO weeks (bucket timestamp = Monday). **Required scope:** `analytics:read` **Rate limit:** 60 requests per minute per API key Dates are inclusive shop-local calendar days (YYYY-MM-DD), with start_date <= end_date and at most 366 days per request. Keep the same date range when comparing reports. Choose a fixed granularity when comparing periods. Ratios such as ROAS and CAC are per-bucket values; recompute whole-period ratios from their totals instead of summing or taking an unweighted average of bucket ratios. (Same note as under `GET /v1/analytics/ad-sets`.) Query parameters: - `start_date` (required): as under `GET /v1/analytics/product-relationships` - `end_date` (required): as under `GET /v1/analytics/profit-loss` - `filter_mode`: as under `GET /v1/analytics/ad-sets` - `filter_ids`: as under `GET /v1/analytics/ad-sets` - `granularity` ("auto" | "daily" | "weekly", default "auto"): Bucket size. `auto` (default) = hourly for very short ranges, daily otherwise; `daily` forces day buckets; `weekly` re-buckets to ISO weeks (bucket timestamp = Monday). Response (success envelope `data`): - `data`: MetricsTimeseriesResponseData - `timeseries`: MetricsTimeseriesPoint[] - `timestamp`: string - `total_orders`: number - `total_revenue`: number - `total_refunds`: number - `total_cogs`: number - `total_ad_spend`: number - `total_payment_fees`: number - `total_shipping_fees`: number - `total_tax`: number - `net_revenue`: number - `cm1`: number - `cm2`: number - `profit`: number - `roas`: number - `new_customer_count`: number - `new_customer_revenue`: number - `new_customer_roas`: number - `cac`: number - `aggregation_level`: "hourly" | "daily" | "weekly" ### Orders Orders — list with financials, and per-order cost breakdowns #### GET /v1/orders List orders Lists Shopify orders in a shop-local date window with per-order financials: gross total, tax, refunds (plus a derived `refund_status`), net revenue, CM1/CM2 contribution margins, first-order flag, customer name, payment gateway, and shipping country. Searchable by order number or customer name, sortable, and paginated. Monetary values are in the shop currency. Venon does not mirror Shopify's `financial_status`; use `refund_status` (none | partial | full, derived from refunded amounts) instead. **Required scope:** `orders:read` **Rate limit:** 60 requests per minute per API key Dates are inclusive shop-local calendar days (YYYY-MM-DD), with start_date <= end_date and at most 366 days per request. Keep the same date range when comparing reports. Use the returned id for an order-detail request; the displayed name/number (for example #1001) is not that ID. Page through results using page/per_page (default 50, maximum 100). Query parameters: - `start_date` (required): as under `GET /v1/analytics/product-relationships` - `end_date` (required): as under `GET /v1/analytics/profit-loss` - `q` (string): Case-insensitive substring match on order number or customer name - `sort_by` ("date" | "total_price" | "order_number", default "date"): Sort field. Default `date` (order timestamp). - `order` ("asc" | "desc", default "desc"): Sort order. Default `desc`. - `page` (integer, default 1): Page number. Default 1. - `per_page` (integer, default 50): Items per page (max 100). Default 50. Response (success envelope `data`): - `data`: OrdersListResponseData - `orders`: OrderSummaryData[] - `id`: string - `order_number`: string | null - `order_timestamp`: string | null - `total_price`: number | null - `total_tax`: number | null - `total_refund_amount`: number | null - `refund_status`: "none" | "partial" | "full" - `net_revenue`: number | null - `cm1`: number | null - `cm2`: number | null - `is_first_customer_order`: boolean | null - `customer_first_name`: string | null - `customer_last_name`: string | null - `payment_gateway`: string | null - `shipping_country`: string | null - `pagination`: PaginationMetadata (fields as under `GET /v1/analytics/ad-sets`) #### GET /v1/orders/{id} Get an order cost breakdown Returns the full cost breakdown of a single order: every line item with its resolved per-unit COGS (variant rule > product rule > Shopify variant cost > global fallback, effective at the order's timestamp) and per-unit profit, plus an order-level summary — gross/net revenue, effective tax, COGS, payment fees, shipping fee with the resolved shipping profile, refunds, recovered COGS/VAT, CM1, CM2, net margin, and break-even ROAS. **Required scope:** `orders:read` **Rate limit:** 60 requests per minute per API key Resolve the order ID from the order list. has_cost_data means at least one cost component resolved, not that every component is complete. Inspect unavailable_metrics and preserve nulls. Line costs are per unit; use quantity when comparing them with the order-level summary. Path parameters: - `id` (integer, required): Shopify order ID (numeric) Response (success envelope `data`): - `data`: OrderCostBreakdownResponseData - `order`: object - `id`: string - `name`: string - `has_cost_data`: boolean - `line_items`: OrderCostLineItem[] - `id`: string - `product_id`: string | null - `variant_id`: string | null - `title`: string - `variant_title`: string | null - `sku`: string | null - `image_url`: string | null - `quantity`: number - `sale_price`: number | null - `product_cost`: number | null - `product_profit`: number | null - `summary`: OrderCostSummary - `gross_revenue`: number | null - `effective_tax`: number | null - `net_revenue`: number | null - `total_cogs`: number | null - `payment_fees`: number | null - `shipping_fees`: number | null - `shipping_profile`: string | null - `shipping_profile_currency`: string | null - `total_refund_amount`: number | null - `recovered_cogs`: number | null - `recovered_vat`: number | null - `cm1`: number | null - `cm2`: number | null - `net_margin`: number | null - `break_even_roas`: number | null - `cm3`: null - `unavailable_metrics`: string[] ### Products Product performance — units, revenue, COGS, and margin per product/variant #### GET /v1/products Get product performance Sales performance per product — or per variant with `level=variant` — over a shop-local date window: units sold, distinct orders, gross line-item revenue (unit price × quantity, before order-level discounts/tax/refunds), resolved COGS, margin, and margin %. Variant rows carry the merchant `sku`, and `q` matches SKUs too — this endpoint doubles as the SKU ↔ variant-ID lookup for integrations. COGS uses the same per-line resolution as order cost breakdowns (variant rule > product rule > Shopify variant cost > global fallback, effective at each order's timestamp); lines without a resolvable cost contribute 0 to `cogs`. Filterable by name, sortable, and paginated; `totals` aggregates the full filtered set. Product status (including ACTIVE/UNLISTED) is not exposed or filterable here. Do not infer status from catalog presence, sales, or soft-deletion filtering. If a task requires status-based selection, obtain an explicit product/variant selection from the user or another authorized source. **Required scope:** `analytics:read` **Rate limit:** 60 requests per minute per API key Dates are inclusive shop-local calendar days (YYYY-MM-DD), with start_date <= end_date and at most 366 days per request. Keep the same date range when comparing reports. Use product-level rows to compare products and variant-level rows for SKU-level analysis. Sales in this window do not establish current catalog availability. totals cover the full filtered result, not just the current page. Query parameters: - `start_date` (required): as under `GET /v1/analytics/product-relationships` - `end_date` (required): as under `GET /v1/analytics/profit-loss` - `level` ("product" | "variant", default "product"): Aggregation level: one row per product (default) or per variant. - `q` (string): Case-insensitive substring match on product/variant name or SKU - `sort_by` ("gross_revenue" | "units" | "orders" | "cogs" | "margin" | "name", default "gross_revenue"): Sort field. Default `gross_revenue`. - `order`: as under `GET /v1/orders` - `page`: as under `GET /v1/orders` - `per_page`: as under `GET /v1/orders` Response (success envelope `data`): - `data`: ProductPerformanceResponseData - `products`: ProductPerformanceData[] - `product_id`: string | null - `product_name`: string | null - `variant_id`: string | null - `variant_title`: string | null - `sku`: string | null - `units`: number - `orders`: number - `gross_revenue`: number - `cogs`: number - `margin`: number - `margin_pct`: number | null - `totals`: ProductPerformanceTotals - `units`: number - `gross_revenue`: number - `cogs`: number - `margin`: number - `pagination`: PaginationMetadata (fields as under `GET /v1/analytics/ad-sets`) #### GET /v1/products/catalog Look up the product catalog Lists every product and variant in the shop — regardless of sales — with variant IDs, titles, merchant SKUs, and prices. This is the resolution surface for turning a SKU or product name into the `product_id` / `variant_id` the other endpoints accept (e.g. before writing a COGS rule): `sku` filters to an exact SKU match, `q` does a fuzzy search over product names, variant titles, and SKUs. Soft-deleted variants are excluded. For sales metrics use `GET /v1/products` instead. (Same note as under `GET /v1/products`.) **Required scope:** `analytics:read` **Rate limit:** 60 requests per minute per API key Pagination counts products, each with a nested variants array; a page is not a flat list of variants. Follow pagination metadata to inspect all matches. A SKU can identify multiple variants: resolve the intended product/variant ID before making cost changes. Query parameters: - `q` (string): Case-insensitive substring match on product name, variant title, or SKU - `sku` (string): Exact SKU match (returns every variant carrying this SKU) - `page`: as under `GET /v1/orders` - `per_page` (integer, default 50): Products per page (max 100). Default 50. Response (success envelope `data`): - `data`: ProductCatalogResponseData - `products`: CatalogProduct[] - `product_id`: string - `product_name`: string - `product_type`: string | null - `variants`: CatalogVariant[] - `variant_id`: string - `variant_title`: string | null - `sku`: string | null - `price`: number | null - `pagination`: PaginationMetadata (fields as under `GET /v1/analytics/ad-sets`) ### Attribution Attribution drill-downs — orders and products a channel/campaign drove, non-ad campaign performance #### GET /v1/attribution/campaigns Get non-ad channel campaign performance Campaign-level attributed performance for a NON-ad channel (email, organic, direct, referral, …): attributed orders and revenue, COGS, payment fees, tax, net profit, profit margin %, AOV, and first-time-customer metrics per campaign (campaign names come from tracking parameters). For paid ad channels use `GET /v1/analytics/campaigns` instead — this endpoint rejects them with 400. **Required scope:** `analytics:read` **Rate limit:** 60 requests per minute per API key Dates are inclusive shop-local calendar days (YYYY-MM-DD), with start_date <= end_date and at most 366 days per request. Keep the same date range when comparing reports. Keep attribution_model and attribution_window identical when comparing channels or drilling down into campaigns, orders and products; changing them changes the credited results. A campaign here is a tracking campaign name, not an ad-platform campaign ID. Discover the channel from channel performance and keep its exact identifier when requesting this breakdown. (Same note as under `GET /v1/analytics/ad-sets`.) Query parameters: - `channel` (string, required): Non-ad channel identifier (e.g. organic, email, direct, referral). For paid ad channels use GET /v1/analytics/campaigns instead. - `start_date` (string, required): Start date (YYYY-MM-DD; orders are selected by order date) - `end_date` (required): as under `GET /v1/analytics/ad-sets` - `attribution_model`: as under `GET /v1/analytics/ad-sets` - `attribution_window`: as under `GET /v1/analytics/ad-sets` - `filter_mode`: as under `GET /v1/analytics/ad-sets` - `filter_ids`: as under `GET /v1/analytics/ad-sets` Response (success envelope `data`): - `data`: NonAdCampaignsResponseData - `campaigns`: NonAdCampaign[] - `channel`: string - `campaign`: string | null - `attributed_orders`: number - `attributed_revenue`: number - `distinct_orders_touched`: number - `attributed_cogs`: number - `attributed_payment_fees`: number - `attributed_tax`: number - `gross_revenue`: number - `net_profit`: number - `profit_margin_pct`: number - `avg_order_value`: number - `revenue_per_order_touched`: number - `first_time_customer_orders`: number - `first_time_customer_revenue`: number #### GET /v1/attribution/orders List attributed orders Lists the individual orders attributed to a channel — optionally narrowed to one campaign / ad set / ad. Answers "which orders did this campaign actually drive?". Paginated (`page`/`per_page`, max 100 per page) with a stable newest-first ordering; `pagination.total_items` carries the full count. **Scoping:** `channel` alone returns the whole channel. On paid ad channels, narrow with the platform IDs `campaign_id` / `ad_set_id` / `ad_id` (the same IDs the /v1/analytics endpoints return); on non-ad channels (organic, email, direct, referral, …) narrow with `campaign` (the campaign name from tracking parameters). Orders are selected by order date using click/window-based attribution — the same selection the dashboard drill-downs use. **Required scopes:** `analytics:read` **and** `orders:read` — this endpoint returns individual order records, so an analytics-only key cannot use it. **Rate limit:** 60 requests per minute per API key Dates are inclusive shop-local calendar days (YYYY-MM-DD), with start_date <= end_date and at most 366 days per request. Keep the same date range when comparing reports. Keep attribution_model and attribution_window identical when comparing channels or drilling down into campaigns, orders and products; changing them changes the credited results. total_price is the gross total of each matched order, not fractional attribution credit. Summing these order totals is not a substitute for the analytics attributed_revenue metric. (Same note as under `GET /v1/analytics/ad-sets`.) Query parameters: - `channel` (string, required): Channel identifier — a paid ad channel (e.g. meta-ads, google-ads) or a non-ad channel (e.g. organic, email, direct). Use GET /v1/analytics/channels to discover the channels active in your shop. - `start_date` (required): as under `GET /v1/attribution/campaigns` - `end_date` (required): as under `GET /v1/analytics/ad-sets` - `attribution_model`: as under `GET /v1/analytics/ad-sets` - `attribution_window`: as under `GET /v1/analytics/ad-sets` - `campaign` (string): Campaign name filter for NON-ad channels (e.g. an email campaign). Ignored for paid ad channels — use campaign_id there. - `campaign_id` (string): Platform campaign ID filter (paid ad channels only) - `ad_set_id` (string): Platform ad-set ID filter (paid ad channels only) - `ad_id` (string): Platform ad ID filter (paid ad channels only) - `first_time_customers_only` ("true" | "false"): Keep only first-time customer orders - `page`: as under `GET /v1/orders` - `per_page`: as under `GET /v1/orders` - `filter_mode`: as under `GET /v1/analytics/ad-sets` - `filter_ids`: as under `GET /v1/analytics/ad-sets` Response (success envelope `data`): - `data`: AttributedOrdersResponseData - `orders`: AttributedOrder[] - `order_id`: string - `order_number`: string - `order_timestamp`: string - `total_price`: number - `is_first_customer_order`: boolean - `pagination`: PaginationMetadata (fields as under `GET /v1/analytics/ad-sets`) #### GET /v1/attribution/products List attributed products Lists the products bought in the orders attributed to a channel — optionally narrowed to one campaign / ad set / ad — aggregated per product+variant with quantity and revenue. Answers "which products does this channel/campaign sell?". Paginated (`page`/`per_page`, max 100 per page), ordered by revenue descending; `pagination.total_items` carries the full count. (Same note as under `GET /v1/attribution/orders`.) **Required scope:** `analytics:read` **Rate limit:** 60 requests per minute per API key Dates are inclusive shop-local calendar days (YYYY-MM-DD), with start_date <= end_date and at most 366 days per request. Keep the same date range when comparing reports. Keep attribution_model and attribution_window identical when comparing channels or drilling down into campaigns, orders and products; changing them changes the credited results. Rows group purchased product/variant combinations; they are not catalog records or individual orders. Use the attributed-orders report when the question needs order identities. (Same note as under `GET /v1/analytics/ad-sets`.) Query parameters: - `channel` (required): as under `GET /v1/attribution/orders` - `start_date` (required): as under `GET /v1/attribution/campaigns` - `end_date` (required): as under `GET /v1/analytics/ad-sets` - `attribution_model`: as under `GET /v1/analytics/ad-sets` - `attribution_window`: as under `GET /v1/analytics/ad-sets` - `campaign`: as under `GET /v1/attribution/orders` - `campaign_id`: as under `GET /v1/attribution/orders` - `ad_set_id`: as under `GET /v1/attribution/orders` - `ad_id`: as under `GET /v1/attribution/orders` - `first_time_customers_only`: as under `GET /v1/attribution/orders` - `page`: as under `GET /v1/orders` - `per_page`: as under `GET /v1/orders` - `filter_mode`: as under `GET /v1/analytics/ad-sets` - `filter_ids`: as under `GET /v1/analytics/ad-sets` Response (success envelope `data`): - `data`: AttributedProductsResponseData - `products`: AttributedProduct[] - `product_id`: string - `product_name`: string - `variant_id`: string | null - `variant_name`: string | null - `sku`: string | null - `image_url`: string | null - `quantity`: number - `revenue`: number - `pagination`: PaginationMetadata (fields as under `GET /v1/analytics/ad-sets`) ### Cost Profit-tracking cost settings — COGS, payment fees, shipping profiles #### POST /v1/cost/cogs/bulk Bulk-set product COGS Applies one COGS rule to many products/variants. Pass dry_run=true to preview without writing. **Required scope:** `cost:write` **Rate limit:** 60 requests per minute per API key At most 100 product/variant targets per call. Each target is applied independently: a successful response can still contain failed entries. Inspect applied and failed before reporting success, and retry only intended failed targets after checking their current state. Dry-run plans report planned, valid, invalid and a sample; the sample is not the complete target list and applied remains 0. Request body (BulkSetCogsBody): - `targets` (BulkCogsTarget[], required): Products/variants to write the COGS rule for (one rule each). - `product_id` (integer): Shopify product id (set this XOR variant_id) - `variant_id` (integer): Shopify variant id (set this XOR product_id) - `mode` ("shopify" | "constant" | "percent", required): Cost mode: shopify (use Shopify cost), constant (absolute), or percent of price. - `value` (number): Cost value. Required for constant (>=0) and percent (0-100); omit/ignored for shopify. - `basis` ("selling"): Percent basis — only 'selling' is supported (required when mode=percent). - `recover_on_return` (boolean | null): Override charge-COGS-on-returns for these targets; null/omit inherits global. - `effective_from` (string): Reserved — period start is implicit (server uses NOW). Accepted but not applied. - `dry_run` (boolean, default false): When true, validate + plan only; performs NO write. Response (success envelope `data`): - `data`: any #### GET /v1/cost/cogs/global Get global COGS settings Returns the shop’s global COGS fallback settings. **COGS model:** each product or variant is a _scope_ with a day-grain timeline of rule periods (SCD). At calculation time a line item resolves its per-unit cost with this precedence: **variant rule → product rule → Shopify variant cost (`mode: "shopify"`) → global fallback percent**. A variant-level rule coexists with a product-level rule on the same product and simply wins for that variant — there is **no separate "inherit from product" toggle**, and none is needed: to give variants of one product different constant costs, create one rule per variant (`{"mode":"constant","value":4.2,"variant_id":…}`) while the product-level rule keeps covering the remaining variants. Exactly one of `product_id`/`variant_id` is set per rule; the create and list endpoints additionally accept `sku` as the scope (resolved to one variant — an ambiguous SKU is rejected with the candidate variant ids, since Shopify does not enforce unique SKUs; resolve names/SKUs to ids via `GET /v1/products/catalog`). Responses expose `mode` as `shopify | constant | percent` (the database stores these as from_shopify | absolute | percent_of_price). **Required scope:** `cost:read` **Rate limit:** 60 requests per minute per API key These are Venon profit-calculation settings. They do not change Shopify product prices or the product/variant rule timelines. Percentages use percentage points: 30 means 30%, not 0.30. Response (success envelope `data`): - `data`: CogsGlobalSettingsResponse - `fallbackPercent`: number | null - `fallbackBasis`: "selling" | "compare_at" - `chargeCogsOnReturns`: boolean #### PUT /v1/cost/cogs/global Update global COGS settings Upserts the shop’s global COGS fallback settings. **Parameter naming:** request parameters are snake_case (e.g. `variant_id`, `as_of`, `effective_to`). The former camelCase spellings are still accepted as **deprecated aliases**; sending both spellings with different values is rejected with a 400. Unknown query parameters are rejected with a 400 listing the accepted names. Response bodies keep their documented camelCase fields. **Required scope:** `cost:write` **Rate limit:** 60 requests per minute per API key (Same note as under `GET /v1/cost/cogs/global`.) Request body (CostCogsGlobalPutBody): - `fallback_percent` (number | null, required): Fallback COGS as % of price when no rule/variant cost resolves; null disables - `fallback_basis` ("selling", required): Price the fallback percent applies to — 'selling' - `charge_cogs_on_returns` (boolean, required): Whether returned items still incur COGS by default Response (success envelope `data`): - `data`: CogsGlobalSettingsResponse (fields as under `GET /v1/cost/cogs/global`) #### GET /v1/cost/cogs/rules List COGS rules Lists per-product/variant COGS rules grouped by scope. Every scope carries the rule effective at `as_of`, the currently active rule, and the full history — each with its `mode`. Filter to one scope with `product_id`, `variant_id`, or `sku` (at most one; e.g. `?variant_id=52778432299272`). (Same note as under `GET /v1/cost/cogs/global`.) (Same note as under `PUT /v1/cost/cogs/global`.) **Required scope:** `cost:read` **Rate limit:** 60 requests per minute per API key Pagination uses limit/offset (limit defaults to 50, maximum 100) over scopes, not page/per_page or individual history rows. as_of selects the effective period; it does not remove other history from the response. An absent explicit rule does not mean the resolved COGS is zero. Query parameters: - `product_id` (integer): Filter to one product scope (at most one of product_id / variant_id) - `variant_id` (integer): Filter to one variant scope (at most one of product_id / variant_id) - `sku` (string): Filter by merchant SKU (at most one of product_id / variant_id / sku) — resolved to one variant; an ambiguous SKU is rejected with the candidate variant ids - `as_of` (string | string): Resolve the effective rule at this historical point - `limit` (integer, default 50): Page size. Default 50. - `offset` (integer | null, default 0): Page offset. Default 0. Response (success envelope `data`): - `data`: CogsRulesListResponse - `scopes`: CogsRuleScopeView[] - `scope`: object - `productId`: number | null - `variantId`: number | null - `effectiveAt`: string - `effective`: CogsRuleRow | null - `id`: number - `productId`: number | null - `variantId`: number | null - `mode`: "shopify" | "constant" | "percent" - `value`: number | null - `basis`: "selling" | "compare_at" - `recoverOnReturn`: boolean | null - `effectiveTo`: string | null - `current`: CogsRuleRow | null (fields as under `GET /v1/cost/cogs/rules`) - `history`: CogsRuleHistoryEntry[] - `id`: number - `effectiveFrom`: string - `mode`: "shopify" | "constant" | "percent" - `value`: number | null - `basis`: "selling" | "compare_at" - `recoverOnReturn`: boolean | null - `totalCount`: number - `limit`: number - `offset`: number #### POST /v1/cost/cogs/rules Create a COGS rule Sets the current COGS rule for one product or variant. Same-mode writes update the active value and preserve history; mode changes reset the entire scope history; an absent active rule is seeded on the shop-local day. For a constant per-unit cost send `{"mode":"constant","value":5.5,"variant_id":…}` (or `"product_id"`, or `"sku"`). **Scope:** exactly one of `product_id` / `variant_id` / `sku`. A variant-level rule coexists with a product-level rule on the same product and **wins for that variant** at calculation time. (Same note as under `GET /v1/cost/cogs/global`.) (Same note as under `PUT /v1/cost/cogs/global`.) **Required scope:** `cost:write` **Rate limit:** 60 requests per minute per API key Same-mode writes update the active value and preserve history. Mode changes reset the entire scope history; missing active rows are seeded on the shop-local day. After an uncertain response, read the scope before retrying. A constant value is a per-unit cost; percent values use percentage points (30 means 30%). Request body (CostCogsRuleCreateBody): - `mode` ("shopify" | "constant" | "percent", required): Cost mode: shopify (use Shopify cost), constant (absolute), percent of price - `value` (number): Cost value. Required for constant (>=0) and percent (0-100); omit for shopify. - `basis` ("selling"): Percent basis — only 'selling' - `recover_on_return` (boolean | null): Override charge-COGS-on-returns for this rule; null/omit inherits global - `product_id` (integer): Product scope - `variant_id` (integer): Variant scope - `sku` (string): Merchant SKU of the target variant (set exactly one of product_id / variant_id / sku). Resolved to one variant id; an ambiguous SKU is rejected with the candidate ids. Response (success envelope `data`): - `data`: CogsRuleScopeView (fields as under `GET /v1/cost/cogs/rules`) #### POST /v1/cost/cogs/rules/bulk Set different COGS rules in one batch Accepts 1–100 unique targets, each with exactly one safe positive integer product_id or variant_id. Only canonical snake_case request fields are accepted; unknown body or query fields are rejected. Results remain in request order and include index and target. Missing and other-shop targets are indistinguishable. One HTTP request replaces up to 100 single-target requests; database writes remain sequential. Same-mode writes update the current value in place and preserve history. A mode change resets ALL history for that scope. An absent active row is seeded on the shop-local calendar day. Dates are day-grain. Dry runs make no rule mutations, return every target validity outcome and history_reset for valid entries, and can become stale before execution. Ownership is checked again by each real write. Entries are independent, without cross-target atomicity. Always inspect every result: a failed sibling does not roll back successful entries. Never automatically replay the batch. WRITE_UNCERTAIN can mean a write committed but readback failed; re-read that target using bulk-read before deciding whether to retry. CONFLICT and INVALID_RULE also require a re-read before retry. applied counts confirmed successes only; uncertain outcomes may have persisted. Read targets first with POST /v1/cost/cogs/rules/bulk-read, preview with dry_run: true, inspect history_reset, then submit dry_run: false. Malformed entries reject the whole request before any writes. Dry-run selected histories have the same 10,000-row ceiling as bulk-read. cost:write is required even for dry_run. The existing shared-value /cogs/bulk endpoint remains available. **Required scope:** `cost:write` **Rate limit:** 60 requests per minute per API key Request body (CogsBulkWriteBody): - `entries` (object[], required) - `mode` ("shopify" | "constant" | "percent", required): Cost mode: shopify (use Shopify cost), constant (absolute), percent of price - `value` (number): Cost value. Required for constant (>=0) and percent (0-100); omit for shopify. - `basis` ("selling"): Percent basis — only 'selling' - `recover_on_return` (boolean | null): Override charge-COGS-on-returns for this rule; null/omit inherits global - `product_id` (integer) - `variant_id` (integer) - `dry_run` (boolean, default false) Response (success envelope `data`): - `data`: CogsBulkWriteResult - `dry_run`: boolean - `planned`: integer - `valid`: integer - `applied`: integer - `failed`: integer - `results`: object[] - Option `success` = true: - `index`: integer - `target`: object - `product_id`: integer - `variant_id`: integer - `success`: true - `history_reset`: boolean - Option `success` = true: - `index`: integer - `target`: object - `product_id`: integer - `variant_id`: integer - `success`: true - Option `success` = false: - `index`: integer - `target`: object - `product_id`: integer - `variant_id`: integer - `success`: false - `error`: object - `code`: "NOT_FOUND" | "CONFLICT" | "INVALID_RULE" | "WRITE_UNCERTAIN" - `message`: string - `requires_reread`: boolean #### POST /v1/cost/cogs/rules/bulk-read Read selected COGS rule histories in bulk Accepts 1–100 unique targets, each with exactly one safe positive integer product_id or variant_id. Only canonical snake_case request fields are accepted; unknown body or query fields are rejected. Results remain in request order and include index and target. Missing and other-shop targets are indistinguishable. One HTTP request replaces up to 100 single-target requests; database writes remain sequential. This POST is a read and does not require cost:write. Owned targets without explicit rules return an empty scope view (current/effective null, history empty); this is not resolved zero cost, and variant/global fallbacks can still apply. as_of accepts a YYYY-MM-DD shop-local day or an ISO instant converted to the shop timezone; omitted means shop-local today. Complete selected histories are returned up to 10,000 total rows, including the active rows. Above that ceiling the whole read fails with 422: use smaller target chunks; histories are never silently truncated. **Required scope:** `cost:read` **Rate limit:** 60 requests per minute per API key Request body (CogsBulkReadBody): - `targets` (object[], required) - `product_id` (integer) - `variant_id` (integer) - `as_of` (string | string): Shop-local YYYY-MM-DD day or ISO instant converted to the shop timezone Response (success envelope `data`): - `data`: CogsBulkReadResult - `results`: object[] - Option `success` = true: - `index`: integer - `target`: object - `product_id`: integer - `variant_id`: integer - `success`: true - `data`: CogsRuleScopeView (fields as under `GET /v1/cost/cogs/rules`) - Option `success` = false: - `index`: integer - `target`: object - `product_id`: integer - `variant_id`: integer - `success`: false - `error`: object - `code`: "NOT_FOUND" - `message`: "Product or variant not found" #### POST /v1/cost/cogs/rules/history Set the full COGS history timeline Declaratively REPLACES one product/variant scope’s entire COGS chain with the supplied ordered periods (ascending by `until`; the final period has until=null = the active row). One `mode` for the whole timeline. Pass dry_run=true to preview without writing. **Required scope:** `cost:write` **Rate limit:** 60 requests per minute per API key Read the existing timeline first and include every period that should survive; omitted periods are removed. until is an exclusive END date, not a start date. A dry run does not reserve state or guarantee a later write succeeds. Request body (SetCogsHistoryBody): - `product_id` (integer): Shopify product id (set this XOR variant_id). - `variant_id` (integer): Shopify variant id (set this XOR product_id). - `mode` ("shopify" | "constant" | "percent", required): Cost mode for EVERY period: shopify, constant (absolute), or percent of price. - `recover_on_return` (boolean | null): Override charge-COGS-on-returns for every period; null/omit inherits global. - `periods` (CogsHistoryPeriod[], required): Full timeline, ascending by `until`. Every period except the last has a finite `until`; the last has until=null (the active row). REPLACES the existing chain for this scope. - `value` (number): Cost value for this period. Required for constant (>=0) / percent (0-100). - `until` (string | null, required): Period END (ISO datetime). null marks the final, currently-active period. - `dry_run` (boolean, default false): When true, validate + read existing + return a plan; performs NO write. Response (success envelope `data`): - `data`: any #### POST /v1/cost/cogs/rules/ops Apply COGS history operations Applies a batch of add/update/delete operations against one scope’s COGS history in one transaction. Each op’s `effective_to` marks when that row’s period ends; the chain is re-stitched afterwards so periods stay contiguous. **Note:** a batch whose deletes remove the scope’s last remaining row currently returns the empty scope view (`current`/`effective` null, empty history) rather than an error. (Same note as under `POST /v1/cost/cogs/rules`.) (Same note as under `PUT /v1/cost/cogs/global`.) **Required scope:** `cost:write` **Rate limit:** 60 requests per minute per API key Use history-row IDs from the selected scope for update/delete operations. The transaction covers this scope only; a sequence of requests for different scopes is not one atomic change. Request body (CostCogsRulesOpsBody): - `product_id` (integer): Product scope - `variant_id` (integer): Variant scope - `ops` (CostCogsHistoryOp[], required): Diff operations against the scope’s history (max 20 per call) - Option `kind` = "delete": - `kind` ("delete", required): Remove one history row - `id` (integer, required): COGS rule (SCD row) id to delete - Option `kind` = "update": - `kind` ("update", required): Edit one history row - `id` (integer, required): COGS rule (SCD row) id to update - `effective_to` (string | string, required): When this row's period ends - `mode` ("shopify" | "constant" | "percent", required): Cost mode of this period: shopify | constant | percent - `value` (number): Cost value. Required for constant (>=0) and percent (0-100); omit for shopify. - `basis` ("selling"): Percent basis — only 'selling' - `recover_on_return` (boolean | null): Override charge-COGS-on-returns; null/omit inherits global - Option `kind` = "add": - `kind` ("add", required): Insert a new past period - `effective_to` (string | string, required): When this row's period ends - `mode` ("shopify" | "constant" | "percent", required): Cost mode of this period: shopify | constant | percent - `value` (number): Cost value. Required for constant (>=0) and percent (0-100); omit for shopify. - `basis` ("selling"): Percent basis — only 'selling' - `recover_on_return` (boolean | null): Override charge-COGS-on-returns; null/omit inherits global Response (success envelope `data`): - `data`: CogsRuleScopeView (fields as under `GET /v1/cost/cogs/rules`) #### GET /v1/cost/cogs/rules/{id} Get a COGS rule Gets a single COGS rule (SCD row) by id. **Required scope:** `cost:read` **Rate limit:** 60 requests per minute per API key The id is a COGS history-row ID returned by the rules list, not a product or variant ID. Use the scope list to inspect the complete timeline before updating or deleting individual rows. Path parameters: - `id` (integer, required) Response (success envelope `data`): - `data`: CogsRuleRow (fields as under `GET /v1/cost/cogs/rules`) #### GET /v1/cost/payment-gateways List payment gateways Lists payment gateways and their cost/fee settings (e.g. `?history=1&name=shopify_payments` for one gateway’s full timeline). (Same note as under `PUT /v1/cost/cogs/global`.) **Required scope:** `cost:read` **Rate limit:** 60 requests per minute per API key By default only active rows are returned; use history=1 in REST or history=true in MCP for historical rows. Use a returned row id to update that gateway. These settings model costs in Venon; they do not change the fees charged by the payment provider. Query parameters: - `history` ("1" | "true"): Return the full fee history instead of only the active rows - `name` (string): Filter to one gateway by name Response (success envelope `data`): - `data`: any #### POST /v1/cost/payment-gateways Upsert a payment gateway Updates a payment gateway’s cost/fee (SCD upsert). Omit `effective_at` to supersede the active row now; supply it to period-split at that date. (Same note as under `PUT /v1/cost/cogs/global`.) **Required scope:** `cost:write` **Rate limit:** 60 requests per minute per API key id is a required existing gateway-row ID that identifies the gateway timeline; this does not create a gateway from a new name. Supply both cost (fixed amount per transaction) and fee (percentage points, so 2.9 means 2.9%). Read back the timeline after changing an effective date. Request body (CostPaymentGatewayUpsertBody): - `id` (integer, required): Id of any existing row in the gateway’s (shop, name) chain - `cost` (number | null, required): Fixed fee per transaction in the shop currency (e.g. 0.35), NOT a percent; or null - `fee` (number | null, required): Percent fee 0-100 (e.g. 2.99 = 2.99%), NOT a fixed amount; or null - `effective_at` (string | string): Omit to supersede the active row now; supply to period-split at this date Response (success envelope `data`): - `data`: any #### POST /v1/cost/payment-gateways/bulk Bulk-set payment fees Upserts many payment gateways in one call. Pass dry_run=true to preview without writing. (Same note as under `PUT /v1/cost/cogs/global`.) **Required scope:** `cost:write` **Rate limit:** 60 requests per minute per API key At most 50 gateways per call. Each target is applied independently: a successful response can still contain failed entries. Inspect applied and failed before reporting success, and retry only intended failed targets after checking their current state. Dry-run plans report planned, valid, invalid and a sample; the sample is not the complete target list and applied remains 0. Request body (CostBulkSetPaymentFeesBody): - `gateways` (CostPaymentGatewayUpsertBody[], required): Payment-gateway upserts to apply (one upsert each) - `id` (integer, required): Id of any existing row in the gateway’s (shop, name) chain - `cost` (number | null, required): Fixed fee per transaction in the shop currency (e.g. 0.35), NOT a percent; or null - `fee` (number | null, required): Percent fee 0-100 (e.g. 2.99 = 2.99%), NOT a fixed amount; or null - `effective_at` (string | string): Omit to supersede the active row now; supply to period-split at this date - `dry_run` (boolean, default false): When true, validate + plan only; performs NO write Response (success envelope `data`): - `data`: any #### POST /v1/cost/payment-gateways/history Set the full payment gateway fee timeline Declaratively REPLACES one gateway’s entire fee chain (by name) with the supplied ordered periods (ascending by `until`; the final period has until=null = the active row). Each period carries cost + fee. Pass dry_run=true to preview without writing. **Required scope:** `cost:write` **Rate limit:** 60 requests per minute per API key (Same note as under `POST /v1/cost/cogs/rules/history`.) Request body (SetPaymentFeeHistoryBody): - `name` (string, required): Payment gateway name (the (shop, name) chain to replace). - `periods` (PaymentFeeHistoryPeriod[], required): Full fee timeline, ascending by `until`; the last has until=null (active). REPLACES the chain. - `cost` (number | null, required): Fixed fee per transaction in the shop currency for this period (NOT a percent). - `fee` (number | null, required): Percent fee 0-100 for this period (NOT a fixed amount). - `until` (string | null, required): Period END (ISO datetime). null marks the final, currently-active period. - `dry_run` (boolean, default false): When true, validate + read existing + return a plan; performs NO write. Response (success envelope `data`): - `data`: any #### POST /v1/cost/payment-gateways/ops Apply payment gateway history operations Applies a batch of add/update/delete operations against one gateway’s fee history in one transaction. Each op’s `effective_to` marks when that row’s period ends; the literal `'infinity'` marks the active row. (Same note as under `PUT /v1/cost/cogs/global`.) **Required scope:** `cost:write` **Rate limit:** 60 requests per minute per API key Use the exact gateway name and row IDs from its history. The batch is atomic for that gateway; reread its timeline after a validation failure before rebuilding the operations. Request body (CostPaymentGatewayOpsBody): - `name` (string, required): Gateway name (the (shop, name) chain to edit) - `ops` (CostPaymentGatewayHistoryOp[], required): Diff operations against the gateway’s fee history (max 20 per call) - Option `kind` = "delete": - `kind` ("delete", required): Remove one history row - `id` (integer, required): Fee row id to delete - Option `kind` = "update": - `kind` ("update", required): Edit one history row - `id` (integer, required): Fee row id to update - `effective_to` (string | string | "infinity", required): When this row's period ends; the literal 'infinity' marks the active row - `cost` (number | null, required): Fixed fee per transaction in the shop currency (e.g. 0.35), NOT a percent; or null - `fee` (number | null, required): Percent fee 0-100 (e.g. 2.99 = 2.99%), NOT a fixed amount; or null - Option `kind` = "add": - `kind` ("add", required): Insert a new period - `effective_to` (string | string | "infinity", required): When this row's period ends; the literal 'infinity' marks the active row - `cost` (number | null, required): Fixed fee per transaction in the shop currency (e.g. 0.35), NOT a percent; or null - `fee` (number | null, required): Percent fee 0-100 (e.g. 2.99 = 2.99%), NOT a fixed amount; or null Response (success envelope `data`): - `data`: any #### GET /v1/cost/shipping-methods List shipping methods Lists shipping-method titles observed on real orders. Use this endpoint only when pricing intentionally differs by method, then copy a returned title byte-for-byte into `shipping_methods`. **Required scope:** `cost:read` **Rate limit:** 60 requests per minute per API key Response (success envelope `data`): - `data`: string[] #### GET /v1/cost/shipping-profiles List shipping profiles Lists ALL shipping profiles (identity + scope, no fees or pagination). Accepts no query parameters. Use GET /v1/cost/shipping-profiles/{id} for costs and history. Shipping-profile scope uniqueness is independent of the profile name. Two non-default profiles conflict when their countries, shipping methods AND products/variants all overlap. An empty array matches every value on that dimension. Any intersecting combination is enough; the complete lists do not need to be equal. A product includes all its variants, so a product profile and one of its variant profiles conflict when country and method scopes also overlap. Distinct variants of the same product can coexist. The catch-all default is excluded from this overlap check; other profiles must narrow at least one dimension. **Required scope:** `cost:read` **Rate limit:** 60 requests per minute per API key Response (success envelope `data`): - `data`: ShippingProfileSummary[] - `id`: string - `name`: string - `isDefault`: boolean - `countryCodes`: string[] - `shippingMethods`: string[] - `products`: object[] - Option `kind` = "product": - `kind`: "product" - `id`: integer - `label`: string - Option `kind` = "variant": - `kind`: "variant" - `id`: integer - `productId`: integer - `label`: string - `currency`: string #### POST /v1/cost/shipping-profiles Upsert a shipping profile Creates or updates a shipping profile’s identity and scope, optionally with its full fee setup via the nested `fees` block — one atomic request creates a fully configured profile (scalar fees plus flat or weight/item bucket axes; omitted values seed 0). On update, `fees` scalars overwrite the current value in place (history kept) and a provided axis REPLACES that axis, resetting its fee history (same semantics as /tiers). This is the single canonical call for creating or editing a profile; /tiers, /fee-history and /bucket-history are narrower cost-timeline utilities on top of it. **Shipping-method scope:** `shipping_methods: []` is the recommended default whenever fees do not intentionally vary by shipping method. An empty array means the profile applies to all current and future shipping methods; it does not disable the profile. Never invent generic values such as `Standard`. For differentiated pricing, first call `GET /v1/cost/shipping-methods` and copy returned titles byte-for-byte. Unknown, whitespace-changed, or case-changed titles receive a 422 response. Methods already saved on an existing profile remain grandfathered until that profile is re-scoped. Shipping-profile scope uniqueness is independent of the profile name. Two non-default profiles conflict when their countries, shipping methods AND products/variants all overlap. An empty array matches every value on that dimension. Any intersecting combination is enough; the complete lists do not need to be equal. A product includes all its variants, so a product profile and one of its variant profiles conflict when country and method scopes also overlap. Distinct variants of the same product can coexist. The catch-all default is excluded from this overlap check; other profiles must narrow at least one dimension. Provide the existing profile id to update; omit it to create. The server never finds an update target by name or matching scope. Updates exclude their own id from conflict detection but still check every other non-default profile in the shop. Names must be unique within the shop and there can be only one default; is_default cannot be changed on an existing profile. Upsert requires the complete identity/scope fields. Omit fees to preserve all costs and history. Provided scalar fees overwrite current values while preserving history; a provided per_order or per_order_return axis REPLACES that axis and resets its history, including for flat axes. Read each conflicting profile by id and inspect its complete scope and costs. Update by id only when it is the intended target; a profile may cover additional countries or products. Otherwise narrow the proposed scope or resolve the ambiguity with the user. Changing the name or retrying unchanged cannot fix a scope conflict. Do not delete or overwrite another profile merely because it conflicts. Read back the final profile after an authorized update. Identity, scope and nested fees commit in one transaction; a conflict rolls back all changes. Profile-scope writes are serialized within the shop, so concurrent overlapping creates cannot both commit. Preflight reads do not reserve a scope. No idempotency key or revision precondition is supported; after an uncertain timeout, re-read before retrying a create. [Worked examples and workflow guide](https://developerdocs.venon.io/shipping-profiles.md). (Same note as under `PUT /v1/cost/cogs/global`.) **Required scope:** `cost:write` **Rate limit:** 60 requests per minute per API key Request body (CostShippingProfileUpsertBody): - `profile` (object, required): Shipping-profile scope uniqueness is independent of the profile name. Two non-default profiles conflict when their countries, shipping methods AND products/variants all overlap. An empty array matches every value on that dimension. Any intersecting combination is enough; the complete lists do not need to be equal. A product includes all its variants, so a product profile and one of its variant profiles conflict when country and method scopes also overlap. Distinct variants of the same product can coexist. The catch-all default is excluded from this overlap check; other profiles must narrow at least one dimension. Provide the existing profile id to update; omit it to create. The server never finds an update target by name or matching scope. Updates exclude their own id from conflict detection but still check every other non-default profile in the shop. Names must be unique within the shop and there can be only one default; is_default cannot be changed on an existing profile. Upsert requires the complete identity/scope fields. Omit fees to preserve all costs and history. Provided scalar fees overwrite current values while preserving history; a provided per_order or per_order_return axis REPLACES that axis and resets its history, including for flat axes. - `id` (string): Existing profile id to update; omit to create. Never matched by name or scope. - `name` (string, required): Unique name within the shop. Changing it does not bypass scope conflicts. - `is_default` (boolean, required): The default profile is a pure catch-all (no scoping) - `country_codes` (string[], required): ISO 3166-1 alpha-2 codes; [] matches every country - `shipping_methods` (string[], required): [] (recommended default) applies to all current and future shipping methods. For differentiated pricing call GET /v1/cost/shipping-methods first and copy titles byte-for-byte. - `products` (object[], required): [] matches every product. A product includes all its variants; overlapping product/variant scopes conflict when countries and methods also overlap. - Option `kind` = "product": - `kind` ("product", required) - `id` (integer, required): Shopify product id - `label` (string) - Option `kind` = "variant": - `kind` ("variant", required) - `id` (integer, required): Shopify variant id - `product_id` (integer): Parent product id - `label` (string) - `currency` (string, required) - `fees` (object): On CREATE omitted chains seed at 0; on UPDATE scalars overwrite the active value and a provided axis payload REPLACES that axis (resets its history). - `packaging_fee` (number) - `picking_first_item_fee` (number) - `picking_following_items_fee` (number) - `other_per_item_fee` (number) - `per_item_return_fee` (number) - `per_order` (object): Ascending buckets; finite max_value boundaries must strictly increase and the LAST bucket must be open-ended (max_value: null). - Option `type` = "flat": - `type` ("flat", required): Single flat cost for the axis - `value` (number, required): Flat cost - Option `type` = "weight": - `type` ("weight", required): Weight-graduated buckets - `weight_display_unit` ("g" | "kg" | "lbs" | "oz", required) - `buckets` (object[], required) - `max_value` (integer | null, required): Upper bound of this bucket (in weight_display_unit for weight sets, item count for item sets). null = open-ended (∞), allowed only on the LAST bucket. - `cost` (number, required): Shipping cost for this bucket - Option `type` = "item": - `type` ("item", required): Item-count-graduated buckets - `buckets` (object[], required) - `max_value` (integer | null, required): Upper bound of this bucket (in weight_display_unit for weight sets, item count for item sets). null = open-ended (∞), allowed only on the LAST bucket. - `cost` (number, required): Shipping cost for this bucket - `per_order_return` (object): Ascending buckets; finite max_value boundaries must strictly increase and the LAST bucket must be open-ended (max_value: null). - Option `type` = "flat": - `type` ("flat", required): Single flat cost for the axis - `value` (number, required): Flat cost - Option `type` = "weight": - `type` ("weight", required): Weight-graduated buckets - `weight_display_unit` ("g" | "kg" | "lbs" | "oz", required) - `buckets` (object[], required) - `max_value` (integer | null, required): Upper bound of this bucket (in weight_display_unit for weight sets, item count for item sets). null = open-ended (∞), allowed only on the LAST bucket. - `cost` (number, required): Shipping cost for this bucket - Option `type` = "item": - `type` ("item", required): Item-count-graduated buckets - `buckets` (object[], required) - `max_value` (integer | null, required): Upper bound of this bucket (in weight_display_unit for weight sets, item count for item sets). null = open-ended (∞), allowed only on the LAST bucket. - `cost` (number, required): Shipping cost for this bucket Response (success envelope `data`): - `data`: ShippingProfileUpsertResult - `id`: integer #### POST /v1/cost/shipping-profiles/bucket-history Set the full cost timeline for one shipping bucket Declaratively REPLACES one existing bucket’s entire cost chain (by bucket_id) with the supplied ordered periods (ascending by `until`; the final period has until=null = the active row). Create buckets first with /tiers. Pass dry_run=true to preview without writing. **Required scope:** `cost:write` **Rate limit:** 60 requests per minute per API key Request body (SetShippingBucketHistoryBody): - `profile_id` (integer, required): Target shipping profile id. - `bucket_id` (integer, required): Id of an existing bucket in this profile (from get_shipping_profile). - `periods` (ShippingBucketHistoryPeriod[], required): Full cost timeline for this bucket, ascending by `until`; the last has until=null (active). REPLACES the chain. - `cost` (number, required): Bucket cost for this period. - `until` (string | null, required): Period END (ISO datetime). null marks the final, currently-active period. - `dry_run` (boolean, default false): When true, validate + read existing + return a plan; performs NO write. Response (success envelope `data`): - `data`: ShippingProfile | ShippingBucketHistoryPlan - Option ShippingProfile: - `id`: string - `name`: string - `isDefault`: boolean - `countryCodes`: string[] - `shippingMethods`: string[] - `products`: object[] - Option `kind` = "product": - `kind`: "product" - `id`: integer - `label`: string - Option `kind` = "variant": - `kind`: "variant" - `id`: integer - `productId`: integer - `label`: string - `currency`: string - `current`: ShippingFees - `perOrderShipping`: ShippingAxis - Option `type` = "flat": - `type`: "flat" - `value`: ShippingTimedValue - Option `type` = "weight": - `type`: "weight" - `bucketSetId`: integer | null - `buckets`: ShippingBucket[] - `displayUnit`: "g" | "kg" | "lbs" | "oz" - Option `type` = "item": - `type`: "item" - `bucketSetId`: integer | null - `buckets`: ShippingBucket[] - `packagingFee`: ShippingTimedValue - `current`: number - `currentId`: integer | null - `history`: object[] - `pickingFirstItemFee`: ShippingTimedValue (fields as under `POST /v1/cost/shipping-profiles/bucket-history`) - `pickingFollowingItemsFee`: ShippingTimedValue (fields as under `POST /v1/cost/shipping-profiles/bucket-history`) - `otherPerItemFee`: ShippingTimedValue (fields as under `POST /v1/cost/shipping-profiles/bucket-history`) - `perOrderReturnFee`: ShippingAxis (fields as under `POST /v1/cost/shipping-profiles/bucket-history`) - `perItemReturnFee`: ShippingTimedValue (fields as under `POST /v1/cost/shipping-profiles/bucket-history`) - Option ShippingBucketHistoryPlan: - `dry_run`: true - `applied`: false - `currency`: string - `note`: string - `periods`: ShippingBucketHistoryPeriod[] - `cost`: number - `until`: string | null #### POST /v1/cost/shipping-profiles/fee-history Set the full timeline for one scalar shipping fee Declaratively REPLACES one scalar fee chain on a profile (by value_name, e.g. packaging_fee) with the supplied ordered periods (ascending by `until`; the final period has until=null = the active row). For graduated weight/item tiers use /tiers or /bucket-history. Pass dry_run=true to preview without writing. **Required scope:** `cost:write` **Rate limit:** 60 requests per minute per API key Request body (SetShippingFeeHistoryBody): - `profile_id` (integer, required): Target shipping profile id. - `value_name` ("per_order_flat" | "per_order_return_flat" | "packaging_fee" | "picking_first_item_fee" | "picking_following_items_fee" | "other_per_item_fee" | "per_item_return_fee", required): Which scalar fee chain to replace (e.g. packaging_fee, per_item_return_fee). - `periods` (ShippingFeeHistoryPeriod[], required): Full timeline for this fee, ascending by `until`; the last has until=null (active). REPLACES the chain. - `value` (number, required): Fee value for this period. - `until` (string | null, required): Period END (ISO datetime). null marks the final, currently-active period. - `dry_run` (boolean, default false): When true, validate + read existing + return a plan; performs NO write. Response (success envelope `data`): - `data`: ShippingProfile | ShippingFeeHistoryPlan - Option ShippingProfile: (fields as under `POST /v1/cost/shipping-profiles/bucket-history`) - Option ShippingFeeHistoryPlan: - `dry_run`: true - `applied`: false - `currency`: string - `note`: string - `periods`: ShippingFeeHistoryPeriod[] - `value`: number - `until`: string | null #### POST /v1/cost/shipping-profiles/tiers Set weight/item shipping tiers Sets graduated weight/item tiers (with costs) on a profile axis in ONE call — e.g. up to 1kg 4.50, up to 5kg 7.00, 5kg+ 12.00 (open-ended top tier = up_to:null). Replaces the axis, which resets its existing fee history; pass dry_run=true to preview without writing. **Required scope:** `cost:write` **Rate limit:** 60 requests per minute per API key Request body (SetShippingTiersBody): - `profile_id` (integer, required): Target shipping profile id. - `axis` ("per_order" | "per_order_return", default "per_order"): Which axis: outbound shipping (per_order) or returns (per_order_return). - `type` ("weight" | "item", required): Tier basis: weight or item count. - `weight_unit` ("g" | "kg" | "lbs" | "oz"): Display unit for weight tiers (default kg); ignored for item tiers. - `tiers` (ShippingTier[], required): Graduated tiers, ascending. Exactly one may be open-ended (up_to:null) and it must be last. - `up_to` (integer | null, required): Upper bound of this tier (kg/g/lbs/oz for weight, count for item). null = open-ended top tier. - `cost` (number, required): Shipping cost for this tier. - `dry_run` (boolean, default false): When true, validate + return a plan only; performs NO write (axis replace is destructive). Response (success envelope `data`): - `data`: ShippingProfile | ShippingTiersPlan - Option ShippingProfile: (fields as under `POST /v1/cost/shipping-profiles/bucket-history`) - Option ShippingTiersPlan: - `dry_run`: true - `applied`: false - `currency`: string - `note`: string - `profile_id`: integer - `axis`: "per_order" | "per_order_return" - `type`: "weight" | "item" - `tiers`: ShippingTier[] - `up_to`: integer | null - `cost`: number #### GET /v1/cost/shipping-profiles/{id} Get a shipping profile Gets one shipping profile with its complete scope, current fees, buckets and each fee/bucket cost history. Response fields remain camelCase. history[].effectiveFrom is the exclusive END of a historical value, when the next value took over. Read each conflicting profile by id and inspect its complete scope and costs. Update by id only when it is the intended target; a profile may cover additional countries or products. Otherwise narrow the proposed scope or resolve the ambiguity with the user. Changing the name or retrying unchanged cannot fix a scope conflict. Do not delete or overwrite another profile merely because it conflicts. Read back the final profile after an authorized update. **Required scope:** `cost:read` **Rate limit:** 60 requests per minute per API key Path parameters: - `id` (required): as under `GET /v1/cost/cogs/rules/{id}` Response (success envelope `data`): - `data`: ShippingProfile (fields as under `POST /v1/cost/shipping-profiles/bucket-history`) #### DELETE /v1/cost/shipping-profiles/{id} Delete a shipping profile Deletes a non-default shipping profile and its fee history. **Required scope:** `cost:write` **Rate limit:** 60 requests per minute per API key Path parameters: - `id` (required): as under `GET /v1/cost/cogs/rules/{id}` Response (success envelope `data`): - `data`: object - `deleted`: true #### POST /v1/cost/shipping-profiles/{id}/cost-updates Update multiple tier costs from an effective date, preserving history Update up to 50 existing shipping buckets in one atomic request, in the profile currency. First preview with dry_run=true, then apply with its revision as expected_revision. effective_from is an inclusive shop-local YYYY-MM-DD date; backdated and future changes are supported. Each new cost lasts until the next existing period boundary (effective_until in the result), or indefinitely if null. Earlier history, later scheduled costs, bucket boundaries and omitted buckets are preserved. At an existing start boundary only that period is updated. Equal costs are no-ops. A missing/cross-profile bucket fails the whole request with 404 and a field-level error. A changed profile returns SHIPPING_PROFILE_REVISION_CONFLICT (409); preview again before applying. Preview does not reserve the profile. There are no idempotency keys or saved responses. After an uncertain timeout, fetch the current profile and reconcile the requested costs. Repeating a committed change with its original revision returns 409; unchanged no-op requests may succeed again. Do not automatically refresh the revision and overwrite newer edits. Preview and review any remaining change before applying. The revision covers identity, scope, tiers and all fee histories. Use /tiers only to replace tier structure and /bucket-history only to explicitly replace a complete timeline. **Required scope:** `cost:write` **Rate limit:** 60 requests per minute per API key Path parameters: - `id` (required): as under `GET /v1/cost/cogs/rules/{id}` Request body (ShippingCostUpdateBody): - `effective_from` (string, required): Inclusive shop-local date (YYYY-MM-DD). Earlier costs and later scheduled periods are preserved. - `bucket_costs` (object[], required): Up to 50 distinct existing buckets from this profile. Omitted buckets are unchanged. - `bucket_id` (integer, required): Existing bucket id from get_shipping_profile; never a history-row id. - `cost` (number, required): New shipping cost in the profile currency. - `expected_revision` (string): Revision returned by a dry run. Required when applying; stale revisions return 409. - `dry_run` (boolean, default false): Preview without writing. Returns the revision required to apply the change. Response (success envelope `data`): - `data`: ShippingCostUpdateResult - `profile_id`: integer - `currency`: string - `effective_from`: string - `dry_run`: boolean - `applied`: boolean - `revision`: string - `bucket_costs`: object[] - `bucket_id`: integer - `previous_cost`: number - `cost`: number - `effective_until`: string | null - `changed`: boolean ### Ads Ads management — campaign / ad-set / ad status and budget writes (Meta + Google) #### PATCH /v1/ads/{channel}/ad-sets/{ad_set_id}/budget Update an ad set daily budget (meta-ads only) Updates the daily budget of a Meta ad set. `meta-ads` only — Google Ads ad groups have no own budget (returns 400); update the campaign budget instead. The budget is the DAILY budget in the ad account currency (e.g. `50` = 50.00). The change is applied on the ad platform immediately and mirrored into Venon. Ids are platform ids as returned by the analytics endpoints. **Required scope:** `ads:write` **Rate limit:** 10 requests per minute per API key budget replaces the daily amount; it is not a delta or a lifetime budget. Send currency units with at most two decimal places, not cents or micros. These operations have no dry_run. The ad account currency may differ from the shop currency used in shop reports. Path parameters: - `channel` ("meta-ads" | "google-ads", required): Ad channel — only Meta and Google Ads support writes. - `ad_set_id` (string, required): Platform ad set id — as returned by the analytics endpoints/tools. Request body (object): - `budget` (number, required): New daily budget in the ad account currency (e.g. 50 = 50.00 EUR/USD), max 2 decimal places. Converted to platform units (cents / micros) server-side. Response (success envelope `data`): - `data`: AdsAdSetResult - `ad_set_id`: string - `channel`: "meta-ads" | "google-ads" - `name`: string - `active`: boolean - `budget`: number - `budget_currency`: string | null #### PATCH /v1/ads/{channel}/ad-sets/{ad_set_id}/status Activate or pause an ad set Activates or pauses a Meta ad set / Google Ads ad group. The change is applied on the ad platform immediately and mirrored into Venon. Ids are platform ids as returned by the analytics endpoints. **Required scope:** `ads:write` **Rate limit:** 10 requests per minute per API key enabled sets the requested state; it is not a toggle. These operations have no dry_run. A successful activation does not guarantee ad delivery, which also depends on parent state and platform eligibility. Google Performance Max child ad sets/ads cannot be edited individually; use their campaign instead. Path parameters: - `channel` (required): as under `PATCH /v1/ads/{channel}/ad-sets/{ad_set_id}/budget` - `ad_set_id` (required): as under `PATCH /v1/ads/{channel}/ad-sets/{ad_set_id}/budget` Request body (object): - `enabled` (boolean, required): true → activate (status ACTIVE/ENABLED), false → pause. Response (success envelope `data`): - `data`: AdsAdSetResult (fields as under `PATCH /v1/ads/{channel}/ad-sets/{ad_set_id}/budget`) #### PATCH /v1/ads/{channel}/ads/{ad_id}/status Activate or pause an ad Activates or pauses a single Meta / Google ad. (Same note as under `PATCH /v1/ads/{channel}/ad-sets/{ad_set_id}/status`.) **Required scope:** `ads:write` **Rate limit:** 10 requests per minute per API key (Same note as under `PATCH /v1/ads/{channel}/ad-sets/{ad_set_id}/status`.) Path parameters: - `channel` (required): as under `PATCH /v1/ads/{channel}/ad-sets/{ad_set_id}/budget` - `ad_id` (string, required): Platform ad id — as returned by the analytics endpoints/tools. Request body (object): - `enabled` (boolean, required): true → activate (status ACTIVE/ENABLED), false → pause. Response (success envelope `data`): - `data`: AdsAdResult - `ad_id`: string - `channel`: "meta-ads" | "google-ads" - `name`: string - `active`: boolean #### PATCH /v1/ads/{channel}/campaigns/{campaign_id}/budget Update a campaign daily budget Updates the daily budget of a Meta / Google Ads campaign. (Same note as under `PATCH /v1/ads/{channel}/ad-sets/{ad_set_id}/budget`.) **Required scope:** `ads:write` **Rate limit:** 10 requests per minute per API key (Same note as under `PATCH /v1/ads/{channel}/ad-sets/{ad_set_id}/budget`.) Path parameters: - `channel` (required): as under `PATCH /v1/ads/{channel}/ad-sets/{ad_set_id}/budget` - `campaign_id` (string, required): Platform campaign id — as returned by the analytics endpoints/tools. Request body (object): - `budget` (number, required): New daily budget in the ad account currency (e.g. 50 = 50.00 EUR/USD), max 2 decimal places. Converted to platform units (cents / micros) server-side. Response (success envelope `data`): - `data`: AdsCampaignResult - `campaign_id`: string - `channel`: "meta-ads" | "google-ads" - `name`: string - `active`: boolean - `budget`: number - `budget_currency`: string | null #### PATCH /v1/ads/{channel}/campaigns/{campaign_id}/status Activate or pause a campaign Activates or pauses a Meta / Google Ads campaign. (Same note as under `PATCH /v1/ads/{channel}/ad-sets/{ad_set_id}/status`.) **Required scope:** `ads:write` **Rate limit:** 10 requests per minute per API key (Same note as under `PATCH /v1/ads/{channel}/ad-sets/{ad_set_id}/status`.) Path parameters: - `channel` (required): as under `PATCH /v1/ads/{channel}/ad-sets/{ad_set_id}/budget` - `campaign_id` (required): as under `PATCH /v1/ads/{channel}/campaigns/{campaign_id}/budget` Request body (object): - `enabled` (boolean, required): true → activate (status ACTIVE/ENABLED), false → pause. Response (success envelope `data`): - `data`: AdsCampaignResult (fields as under `PATCH /v1/ads/{channel}/campaigns/{campaign_id}/budget`) ### Data Filters Saved data filters: the dashboard report filters, and how to apply them to report endpoints #### GET /v1/data-filters List saved data filters Lists the shop's saved data filters (dashboard Settings > Data Filters) with their conditions and enabled state. Pass their ids to a report endpoint with `filter_mode=selected` to apply them. **Required scope:** `filters:read` **Rate limit:** 60 requests per minute per API key A saved data filter belongs to one source: shopify or an ad channel. Conditions in a group must all match and at least one group must match: (A AND B) OR (C AND D). Shopify fields: order_name (contains, does_not_contain, starts_with, is_any_of, is_none_of), shipping_country (ISO 3166-1 alpha-2 code) and payment_gateway (is_any_of, is_none_of). Ad fields: campaign_name, ad_set_name, ad_name (all operators) and campaign_id, ad_set_id, ad_id (platform ids; is_any_of, is_none_of). Matching ignores case; a missing value fails is_any_of, contains and starts_with and passes is_none_of and does_not_contain. At most 10 groups and 10 conditions in total, 100 values of up to 256 characters per condition, 5,000 bytes of values per filter and 20 saved filters per shop. An update replaces the whole filter. A stored filter that no longer passes these rules or limits is left out of lists and reports, and the response warns about it (warnings: how many filters are stored and which were skipped, by id and reason) instead of failing. Response (success envelope `data`): - `data`: DataFilterList - `data_filters`: DataFilter[] - `source`: "shopify" | "meta-ads" | "google-ads" | "taboola" | "tiktok-ads" | "outbrain" | "microsoft-ads" | "pinterest-ads" - `title`: string - `description`: string | null - `enabled`: boolean - `groups`: object[][] - `field`: "campaign_name" | "ad_set_name" | "ad_name" | "order_name" | "campaign_id" | "ad_set_id" | "ad_id" | "shipping_country" | "payment_gateway" - `operator`: "is_any_of" | "is_none_of" | "contains" | "does_not_contain" | "starts_with" - `values`: string[] - `id`: string #### POST /v1/data-filters Create a saved data filter Creates a saved data filter. An enabled filter immediately narrows the dashboard and Pixel reports of every user of this shop. **Required scope:** `filters:write` **Rate limit:** 60 requests per minute per API key (Same note as under `GET /v1/data-filters`.) Request body (DataFilterInput): - `source` ("shopify" | "meta-ads" | "google-ads" | "taboola" | "tiktok-ads" | "outbrain" | "microsoft-ads" | "pinterest-ads", required) - `title` (string, required) - `description` (string | null, required) - `enabled` (boolean, required) - `groups` (object[][], required) - `field` ("campaign_name" | "ad_set_name" | "ad_name" | "order_name" | "campaign_id" | "ad_set_id" | "ad_id" | "shipping_country" | "payment_gateway", required) - `operator` ("is_any_of" | "is_none_of" | "contains" | "does_not_contain" | "starts_with", required) - `values` (string[], required) Response (success envelope `data`): - `data`: DataFilter (fields as under `GET /v1/data-filters`) #### PUT /v1/data-filters/{id} Replace a saved data filter Replaces a saved data filter (title, description, enabled, source and all groups). Set `enabled` to switch it on or off for the dashboard. **Required scope:** `filters:write` **Rate limit:** 60 requests per minute per API key (Same note as under `GET /v1/data-filters`.) Path parameters: - `id` (string, required): Saved data filter id Request body (DataFilterInput): - `source` ("shopify" | "meta-ads" | "google-ads" | "taboola" | "tiktok-ads" | "outbrain" | "microsoft-ads" | "pinterest-ads", required) - `title` (string, required) - `description` (string | null, required) - `enabled` (boolean, required) - `groups` (object[][], required) - `field` ("campaign_name" | "ad_set_name" | "ad_name" | "order_name" | "campaign_id" | "ad_set_id" | "ad_id" | "shipping_country" | "payment_gateway", required) - `operator` ("is_any_of" | "is_none_of" | "contains" | "does_not_contain" | "starts_with", required) - `values` (string[], required) Response (success envelope `data`): - `data`: DataFilter (fields as under `GET /v1/data-filters`) #### DELETE /v1/data-filters/{id} Delete a saved data filter Deletes a saved data filter; the dashboard and Pixel reports stop applying it. **Required scope:** `filters:write` **Rate limit:** 60 requests per minute per API key (Same note as under `GET /v1/data-filters`.) Path parameters: - `id` (required): as under `PUT /v1/data-filters/{id}` Response (success envelope `data`): - `data`: object - `deleted`: true