---
title: "Upload your results per campaign"
method: POST
path: "/v1/ads/campaign-results"
tags: ["Ad Campaign Results"]
---

# Upload your results per campaign

`POST /v1/ads/campaign-results`

## Request

- Base URL: `https://zernio.com/api`
- URL: `https://zernio.com/api/v1/ads/campaign-results`
- Auth: HTTP bearer

Sends your own daily numbers per campaign (qualified leads, sales, revenue...) so rules and reports can use them next to spend.

Name each row's campaign by `campaignId`, `utmCampaign` or `campaignName`. Rows are matched in the background: the `202` returns an `uploadId`, and `Location` points to `GET /v1/ads/campaign-results/uploads/{uploadId}`.

Re-sending a day replaces it. Send an `Idempotency-Key` to retry safely. Limits per team: 60 requests and 20,000 rows per hour, 512 KB per request.

## Headers

- `Idempotency-Key` string

## Request body

- object
  - `rows` object[], required
    - `date` string, date, required: The day, at most 400 days ago.
    - `campaignId` string: The platform campaign id.
    - `utmCampaign` string: The `utm_campaign` value on the campaign's ads.
    - `campaignName` string: The campaign's name, matched case-insensitively.
    - `adAccountId` string: Platform ad account id. Narrows matching to one ad account.
    - `values` object, required: Metric key (snake_case, up to 40 characters) to a non-negative number. 1 to 20 per row.

## Response `202`

Queued for matching.

- object
  - `uploadId` string, required
  - `rows` integer, required
  - `status` 'queued', required

## Other responses

- `400`: Invalid request
- `401`: Missing or invalid API key. `code` is `missing_credentials` when no Authorization header was sent and `invalid_credentials` when the key is unknown, revoked or expired.
- `403`: Code `ads_addon_required` when the team has no ads access.
- `409`: The same `Idempotency-Key` is still being processed. Retry shortly.
- `413`: Code `payload_too_large`: the body is over 512 KB.
- `422`: The `Idempotency-Key` was used with a different body.
- `429`: The connected account's upstream platform quota is exhausted. Behavior per platform: - **Reddit:** rate-limits per connected Reddit user (1000 requests per 10-minute window), shared by every operation on that account. Retry after the window resets; repeated calls while exhausted do not succeed and keep the budget spent. - **Google Ads:** writes and reports run on one developer token shared by every Google Ads account on Zernio. It holds Standard access (no daily operations cap), so this only happens when Google throttles the token or your ad account. - **Google Ads envelope:** `code: rate_limited`, `platform: google`, `details.quotaScope: DEVELOPER` (`ACCOUNT` when it is your own ad account's quota), `details.resetsAt` (ISO instant when Google accepts requests again) and `Retry-After` counting down to it. Retrying earlier cannot succeed. - **YouTube Analytics:** reads (channel-insights, demographics, daily-views, video-retention) use the "Queries per day" quota of the Google Cloud project the account is connected through, shared with other Zernio customers, so a 429 is not your own volume. Resets at midnight Pacific Time. - **YouTube Analytics envelope:** `code: rate_limited`, `platform: google`, `details.quotaExhausted: true`, `details.quotaScope: DEVELOPER`, `details.resetsAt` and `Retry-After` counting down to it. Retrying earlier cannot succeed. - **Meta ads:** every Meta throttle (codes 4, 17, 32, 613 and business-use-case codes 80000-80014) returns 429 `rate_limited`, even when Meta answers HTTP 400. `Retry-After` comes from Meta's `x-business-use-case-usage` estimate when sent, else Meta's 60-second minimum (30 seconds for the one-write-per-30-seconds limit on one object). - **Pinterest ads:** limits per user and app, per category (ads_read, ads_write, ads_analytics). `Retry-After` comes from Pinterest's `x-ratelimit-reset` when sent, otherwise 60 seconds. - **TikTok ads:** limits per app and per endpoint (per second, minute and day), answering overage with code 40100 or code 40002 `OVER_QPS_LIMIT`. Both return 429 `rate_limited` with `platform: tiktok` and `Retry-After: 10`. Space out bulk reads (e.g. one ad at a time per advertiser) instead of retrying at once. **TikTok Smart+ review read** (`GET /v1/ads/{adId}/tiktok-smart-plus-review`) has its own stricter TikTok quota. After an overage, Zernio answers 429 for that advertiser and connection without calling TikTok for about 60 seconds, then 120, up to 240 while TikTok keeps refusing, each with up to 10% random jitter. A successful call holds nothing back; the next overage pauses about 60 seconds again. `Retry-After` is the time left. The advertiser-wide read (`GET /v1/ads/tiktok-smart-plus-reviews`) answers 200 with `retryAfterSeconds` and `refreshState` instead. Keep one request in flight per advertiser there.

## Changes

- **2026-10-30** `c319eb5d95b6`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/zernio/apis/zernio-api/changes/v1/ads/campaign-results/post.md)

---

[API](https://skmtc.dev/zernio/apis/zernio-api.md) - [All operations](https://skmtc.dev/zernio/apis/zernio-api/llms.txt) - [OpenAPI document](https://skmtc.dev/zernio/apis/zernio-api/revisions/c319eb5d95b6?raw)
