# Create a conversion destination API Reference

Creates a new place to send conversion events: a Google Ads conversion action or a LinkedIn conversion rule.

Supported for LinkedIn (conversion rule) and Google Ads (conversion action). It returns 405 for:

- Meta and OpenAI Ads: their pixels are created via their own tracking-tags flow instead (`POST /v1/accounts/{accountId}/tracking-tags`)
- Reddit: create Reddit pixels in Reddit Ads Manager

### LinkedIn

Creation is NOT idempotent. A retry creates a second destination. Deduplicate before retrying.

The rule is created with `conversionMethod=CONVERSIONS_API` and (by default) auto-associated with all of the ad account's campaigns via `autoAssociationType=ALL_CAMPAIGNS`. Pass `autoAssociationType: NONE` to opt out and manage associations explicitly via the associations endpoints below.

365-day attribution windows are only valid for these rule types; the API rejects other combinations locally:

- `SUBMIT_APPLICATION`
- `PURCHASE`
- `ADD_TO_CART`
- `QUALIFIED_LEAD`
- `LEAD`

### Google Ads

Calling with a name that already exists reuses the existing conversion action transparently (the response is identical to a fresh create).

Calling with the same name but a different category, or a name already used by a website (`WEBPAGE`) action, returns a typed `IDEMPOTENCY_CONFLICT` (409) rather than silently returning the mismatched action.

The conversion action is created with `type=UPLOAD_CLICKS` (required for API-uploaded offline conversions, immutable after creation).

The `type` field carries the Google `ConversionActionCategory` enum value, e.g. `PURCHASE`, `SUBSCRIBE_PAID`, `SIGNUP`, `IMPORTED_LEAD`, `BOOK_APPOINTMENT`. Unified standard event names (e.g. `Purchase`, `Subscribe`, `CompleteRegistration`, `Lead`, `Schedule`) are resolved to their Google category equivalents automatically.

The action defaults to secondary (non-primary) to avoid immediately steering Smart Bidding; pass `primaryForGoal: true` to opt in.


## GET /v1/ads/conversions/destinations

**List conversion destinations**

Lists where an ad account can receive conversion events (pixels, conversion actions or rules), so you know where to send them.

Returns the list of pixels (Meta), conversion actions (Google),
conversion rules (LinkedIn), pixels (OpenAI Ads, Reddit) or ad accounts
(Pinterest) accessible to the connected ads account. Use the returned `id` as `destinationId` when
posting to `POST /v1/ads/conversions`.

For Google and LinkedIn, each destination's `type` reflects the
conversion type (PURCHASE, LEAD, SIGN_UP, etc.), and the event type is
locked to the destination. For Meta, OpenAI Ads and Reddit, `type` is absent:
pixels accept any event name per request.

For Reddit, the pixels of every ad account the connection reaches (only the scoped
ones when the connection has an ad-account scope), each with the `adAccountId` that owns
it. Reddit pixels are created in Reddit Ads Manager, not through Zernio.

For LinkedIn, destinations are returned across every sponsored ad
account the connected token can access; the `adAccountId` field on
each destination identifies the parent ad account and is required for
subsequent CRUD calls (update, delete, associations, metrics).


### Parameters

- **accountId** (required) in query: SocialAccount ID (metaads, googleads, linkedinads, tiktokads, openaiads, pinterestads, or redditads).

### Responses

#### 200: Destinations listed

**Response Body:**

- **platform** `string`: No description - one of: metaads, googleads, linkedinads, tiktokads, openaiads, pinterestads, redditads
- **destinations** `array[object]`: 
  - **id** `string`: Destination identifier. Meta: pixel ID. Google:
conversion action resource name. LinkedIn:
numeric conversion rule ID. OpenAI Ads: pixel wire
id. Reddit: pixel id (`a2_...`).

  - **name** `string`: No description
  - **type** `string`: Present when the platform locks event type to the
destination (Google conversion actions, LinkedIn
conversion rules).

  - **status** `string`: No description - one of: active, inactive
  - **adAccountId** `string`: Set by adapters whose destinations are scoped to a
specific ad account (LinkedIn, Reddit). On LinkedIn,
pass it back on subsequent CRUD calls.

  - **conversionEvents** `array[object]`: OpenAI Ads only: the conversion event settings wired
to this pixel. Pass one's `id`, `eventType` or `name`
as `promotedObject.customEventType` on a
`goal: conversions` create (POST /v1/ads/create or
POST /v1/ads/campaigns) to optimize for it. Without
it, the account's most recently created `optimizable`
event is used; when none is, the create returns 400.

    - **id** `string`: No description
    - **name** `string`: No description
    - **eventType** `string,null`: OpenAI event type, e.g. order_created, lead_created, or custom.
    - **customEventName** `string,null`: No description
    - **archived** `boolean`: No description
    - **optimizable** `boolean`: Active and standard (not custom). Only these can be a conversions campaign's optimization event.

#### 400: Account's platform is not supported by the Conversions API.

#### 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.

**Response Body:**

- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **docUrl** `string`: Documentation page for resolving the error, when one applies.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.

- **provider** `string`: Email API: the mailbox provider the error came from (`google`, `microsoft`).
- **providerError** `object`: Email API: the provider's own error code and message, for support and logs. Branch on `code`, not on this.
  - **code** `string,null`: No description
  - **message** `string,null`: No description
- **retryable** `boolean`: Email API: whether the same request can succeed if retried (with the same `Idempotency-Key` for sends).
- **requestId** `string`: Email API: the request id, also in the `X-Request-Id` header. Quote it when you contact support.
- **docsUrl** `string`: Email API: where this `code` is documented.
- **details** `object`: Additional structured context for the error (for example field-level validation errors). What it carries depends on the endpoint and platform.

**Meta ads refusals.** The ones customers hit most come back with their own `code` instead of `platform_api_error`, the field to fix in `param`, and an English `details.hint` saying how to fix it. `details.fix` ({ label, url }) links the Meta page that resolves it, when there is one.

The code is picked from Meta's numeric `code` and `error_subcode`, never from the message, so it is the same whatever language Meta wrote `error_user_msg` in. Branch on `code` and show `details.hint`.

- `ad_account_payment_required`: the ad account has no usable payment method (subcode 1815010, 1359188).
- `ad_account_policy_not_accepted`: a business admin has to accept Meta's non-discrimination policy (subcode 2859002, 2859024).
- `lead_ads_terms_not_accepted`: the Facebook Page has not accepted the Lead Ads terms (subcode 1815089).
- `verified_advertiser_required`: the market requires a verified beneficiary and payer; send `regionalRegulationIdentities` (subcode 3858634, 3858636, 3858442, 3858548, 3858495).
- `ad_account_unusable`: the ad account is not active, or the business is restricted from advertising (subcode 1885316, 2446325, and 2490592, 4841021 below).
- `meta_security_check_required`: the Facebook user behind the connection is held at a security check (code 31).
- `meta_action_blocked`: Meta temporarily blocked the Facebook user from this action (subcode 2859015).
- `meta_ad_consent_required`: the Page or Instagram owner chose Meta without ads or less personalized ads (subcode 3858231, 3858412).
- `instagram_account_restricted`: Meta restricted the Instagram account the ad runs as (subcode 2207070).
- `insufficient_permissions`: the ad account cannot use the Instagram account, or the Facebook user has no advertiser permission on the Page (subcode 1815199, 1815202, 1885499, 1341012, and the Advertise permission case below).
- `whatsapp_number_not_linked`: the WhatsApp number is no longer linked to the Page (subcode 2446880).
- `invalid_targeting`: Meta refused the audience or a placement (subcode 1487079, 1870247, 2909035, 1870189, 1870188, 1487756, 1487843, 2490562).
- `post_not_boostable`: Meta does not allow the post to be promoted (subcode 2446187, 2875030, 1487472).
- `budget_too_low`: the budget is below Meta's minimum, which `platformError.error_user_msg` states (subcode 1885272).
- `meta_object_not_accessible`: Meta cannot load an object the request names (wrong ID, deleted, or no access); `platformError.message` names the ID (subcode 33, on ads routes only).

Meta's own error stays in the message and in `platformError`.

**Private replies.** `privateReplyConsumed` appears on the private-reply endpoint's 400 when the comment's single reply is already spent. `privateReplyOutcome: "unknown"` appears on its 502 when Meta answered a server error and the reply may have been delivered.

**Google quota 429.** On a Google Ads or YouTube Analytics 429, `quotaExhausted: true` marks the failure as Google's own quota, not a Zernio rate limit, so you can keep calling other platforms instead of backing off everywhere. When Google names the scope, `quotaScope` is also set:

- `DEVELOPER`: the shared budget (the developer token for Google Ads, the Google Cloud project for YouTube Analytics). Every account on Zernio is affected and there is nothing to change on your side.
- `ACCOUNT`: your own ad account (Google Ads only).

A Meta 429 carries neither field.

**Zernio Google Ads burst-limit 429.** Carries `budgetScope` instead, never `quotaExhausted`. These are Zernio's own limits, applied before the call reaches Google, and both clear within a minute. They are separate axes from `quotaScope`, not the same pool named twice.

- `account`: the limit of 15 requests per minute per connected Google Ads account.
- `user`: the ceiling of 120 requests per minute per Zernio user across all their Google Ads accounts.

**Failed Meta ad create** (`POST /v1/ads/create`, `POST /v1/ads/boost`, `POST /v1/ads/ctwa`). Carries `stage` (where it failed), `adAccountId` (on which ad account) and `createdObjects` (every object this request had already created, with what cleanup did to it).

- `left_behind` objects still exist on the ad account (Meta refused the delete, typically on a held account, or the delete hit a network error). Delete them yourself or reuse them.
- `unconfirmedWrite` is set when Meta answered a create with a 5xx or a non-JSON body, or dropped the connection, and Zernio could not confirm whether the object exists. Check that parent before creating it again.

**Ineligible ad account: 422 `ad_account_unusable`.** When Meta refuses a write because the ad account's status is ineligible to manage ads (Meta subcode 2490592 or 4841021, on the write itself or on a cleanup delete), any Meta ads write answers it.

That covers create, boost, CTWA, campaign / ad set / ad updates and status changes.

The message names the ad account when known, links Meta's fix (https://www.facebook.com/business/help/422289316306981), lists the objects left behind and ends with Meta's original error, which also stays verbatim in `platformError`.

Meta often reports the first failure under an unrelated code (e.g. 1487194 on the creative), so branch on `code`, not on `platformError`.

**Missing Advertise permission: 403 `insufficient_permissions`.** When the Facebook user behind the connection's token lacks the Advertise permission on the ad account, any Meta ads write answers it.

Triggers: Meta subcode 2490585, 2490591 or 4841020 on the write, or a code 200 refusal such as 1487194 whose cleanup deletes Meta refused with 2490591 or 4841020.

Meta may still accept the campaign and ad set and refuse only the ad. The message names the ad account, asks for the Advertise or Manage permission on it (or a token granted `ads_management`, by reconnecting) and lists the objects left behind.

It ends with Meta's `error_user_title`, `error_user_msg` and subcode; the raw error stays in `platformError`.

  - **stage** `string`: Meta ad create failures only. The step that failed:

- `media`: image/video download or upload.
- `campaign`
- `adset`
- `creative`
- `ad`: the ad POST itself, where Meta's code 31 / 3858385 hold and 100 / 1359188 payment rejections land.
- `activation`: switching the created objects on. No longer returned: a refused switch to ACTIVE now answers 201 with an `activation_failed:` warning and the objects kept PAUSED.
- `other`: a read or check before any write.
 - one of: media, campaign, adset, creative, ad, activation, other
  - **adAccountId** `string`: Meta ad create failures only. The ad account the request wrote to (`act_...`).
  - **createdObjects** `array[object]`: Meta ad create failures only. Every object this request created before failing, in creation order. Objects you referenced (an existing campaign, ad set, creative or video) are never listed and never deleted.
    - **type** `string`: No description - one of: campaign, adset, creative, ad, video, image
    - **id** `string`: Meta object id; for `image` the image hash.
    - **cleanup** `string`: - `deleted`: Zernio deleted it.
- `left_behind`: cleanup could not delete it (Meta refused the delete, or the delete hit a network error), so it still exists.
- `kept`: deliberately not deleted. Either an image (hashes are shared by every upload of the same file), or an object Zernio found by name after Meta lost its answer to the create, which may belong to another request with the same name.
 - one of: deleted, left_behind, kept
  - **unconfirmedWrite** `object`: Meta ad create failures only. Set when Meta answered a create with a 5xx or a non-JSON body, or dropped the connection after the request was sent, and reading the parent back by name did not settle it. Zernio did not retry it.

One of these left it open:

- The read-back found several objects of the same name.
- The parent could not be read, or the object is a creative, which has no reliable read-back.
- The read-back found no such object, after a dropped connection, a non-JSON body, a 5xx with `retryOnServerError: false`, or a 5xx on a create whose own body is `status: ACTIVE`.

    - **type** `string`: No description - one of: campaign, adset, creative, ad
    - **parentId** `string`: Where to look for it: the ad set for an ad, the campaign for an ad set, the ad account otherwise.
  - **quotaExhausted** `boolean`: Google Ads and YouTube Analytics 429 only. True when the upstream Google quota is spent rather than a Zernio limit.
  - **quotaScope** `string`: Google Ads and YouTube Analytics 429 only, when the scope is known. DEVELOPER is the shared budget (the developer token for Google Ads, the Google Cloud project for YouTube Analytics); ACCOUNT is your ad account (Google Ads only). - one of: DEVELOPER, ACCOUNT
  - **budgetScope** `string`: Zernio Google Ads burst-limit 429 only (never set alongside `quotaExhausted`). `account` is the limit of 15 requests per minute per connected Google Ads account; `user` is the ceiling of 120 requests per minute per Zernio user across all their Google Ads accounts. Both clear within a minute. - one of: account, user

#### 403: Ads access required (Ads add-on on legacy plans, included on usage-based plans),
OR (for LinkedIn) the connected account lacks the `rw_conversions` scope and must be reconnected.


#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.

**Response Body:**

- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **docUrl** `string`: Documentation page for resolving the error, when one applies.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.

- **provider** `string`: Email API: the mailbox provider the error came from (`google`, `microsoft`).
- **providerError** `object`: Email API: the provider's own error code and message, for support and logs. Branch on `code`, not on this.
  - **code** `string,null`: No description
  - **message** `string,null`: No description
- **retryable** `boolean`: Email API: whether the same request can succeed if retried (with the same `Idempotency-Key` for sends).
- **requestId** `string`: Email API: the request id, also in the `X-Request-Id` header. Quote it when you contact support.
- **docsUrl** `string`: Email API: where this `code` is documented.
- **details** `object`: Additional structured context for the error (for example field-level validation errors). What it carries depends on the endpoint and platform.

**Meta ads refusals.** The ones customers hit most come back with their own `code` instead of `platform_api_error`, the field to fix in `param`, and an English `details.hint` saying how to fix it. `details.fix` ({ label, url }) links the Meta page that resolves it, when there is one.

The code is picked from Meta's numeric `code` and `error_subcode`, never from the message, so it is the same whatever language Meta wrote `error_user_msg` in. Branch on `code` and show `details.hint`.

- `ad_account_payment_required`: the ad account has no usable payment method (subcode 1815010, 1359188).
- `ad_account_policy_not_accepted`: a business admin has to accept Meta's non-discrimination policy (subcode 2859002, 2859024).
- `lead_ads_terms_not_accepted`: the Facebook Page has not accepted the Lead Ads terms (subcode 1815089).
- `verified_advertiser_required`: the market requires a verified beneficiary and payer; send `regionalRegulationIdentities` (subcode 3858634, 3858636, 3858442, 3858548, 3858495).
- `ad_account_unusable`: the ad account is not active, or the business is restricted from advertising (subcode 1885316, 2446325, and 2490592, 4841021 below).
- `meta_security_check_required`: the Facebook user behind the connection is held at a security check (code 31).
- `meta_action_blocked`: Meta temporarily blocked the Facebook user from this action (subcode 2859015).
- `meta_ad_consent_required`: the Page or Instagram owner chose Meta without ads or less personalized ads (subcode 3858231, 3858412).
- `instagram_account_restricted`: Meta restricted the Instagram account the ad runs as (subcode 2207070).
- `insufficient_permissions`: the ad account cannot use the Instagram account, or the Facebook user has no advertiser permission on the Page (subcode 1815199, 1815202, 1885499, 1341012, and the Advertise permission case below).
- `whatsapp_number_not_linked`: the WhatsApp number is no longer linked to the Page (subcode 2446880).
- `invalid_targeting`: Meta refused the audience or a placement (subcode 1487079, 1870247, 2909035, 1870189, 1870188, 1487756, 1487843, 2490562).
- `post_not_boostable`: Meta does not allow the post to be promoted (subcode 2446187, 2875030, 1487472).
- `budget_too_low`: the budget is below Meta's minimum, which `platformError.error_user_msg` states (subcode 1885272).
- `meta_object_not_accessible`: Meta cannot load an object the request names (wrong ID, deleted, or no access); `platformError.message` names the ID (subcode 33, on ads routes only).

Meta's own error stays in the message and in `platformError`.

**Private replies.** `privateReplyConsumed` appears on the private-reply endpoint's 400 when the comment's single reply is already spent. `privateReplyOutcome: "unknown"` appears on its 502 when Meta answered a server error and the reply may have been delivered.

**Google quota 429.** On a Google Ads or YouTube Analytics 429, `quotaExhausted: true` marks the failure as Google's own quota, not a Zernio rate limit, so you can keep calling other platforms instead of backing off everywhere. When Google names the scope, `quotaScope` is also set:

- `DEVELOPER`: the shared budget (the developer token for Google Ads, the Google Cloud project for YouTube Analytics). Every account on Zernio is affected and there is nothing to change on your side.
- `ACCOUNT`: your own ad account (Google Ads only).

A Meta 429 carries neither field.

**Zernio Google Ads burst-limit 429.** Carries `budgetScope` instead, never `quotaExhausted`. These are Zernio's own limits, applied before the call reaches Google, and both clear within a minute. They are separate axes from `quotaScope`, not the same pool named twice.

- `account`: the limit of 15 requests per minute per connected Google Ads account.
- `user`: the ceiling of 120 requests per minute per Zernio user across all their Google Ads accounts.

**Failed Meta ad create** (`POST /v1/ads/create`, `POST /v1/ads/boost`, `POST /v1/ads/ctwa`). Carries `stage` (where it failed), `adAccountId` (on which ad account) and `createdObjects` (every object this request had already created, with what cleanup did to it).

- `left_behind` objects still exist on the ad account (Meta refused the delete, typically on a held account, or the delete hit a network error). Delete them yourself or reuse them.
- `unconfirmedWrite` is set when Meta answered a create with a 5xx or a non-JSON body, or dropped the connection, and Zernio could not confirm whether the object exists. Check that parent before creating it again.

**Ineligible ad account: 422 `ad_account_unusable`.** When Meta refuses a write because the ad account's status is ineligible to manage ads (Meta subcode 2490592 or 4841021, on the write itself or on a cleanup delete), any Meta ads write answers it.

That covers create, boost, CTWA, campaign / ad set / ad updates and status changes.

The message names the ad account when known, links Meta's fix (https://www.facebook.com/business/help/422289316306981), lists the objects left behind and ends with Meta's original error, which also stays verbatim in `platformError`.

Meta often reports the first failure under an unrelated code (e.g. 1487194 on the creative), so branch on `code`, not on `platformError`.

**Missing Advertise permission: 403 `insufficient_permissions`.** When the Facebook user behind the connection's token lacks the Advertise permission on the ad account, any Meta ads write answers it.

Triggers: Meta subcode 2490585, 2490591 or 4841020 on the write, or a code 200 refusal such as 1487194 whose cleanup deletes Meta refused with 2490591 or 4841020.

Meta may still accept the campaign and ad set and refuse only the ad. The message names the ad account, asks for the Advertise or Manage permission on it (or a token granted `ads_management`, by reconnecting) and lists the objects left behind.

It ends with Meta's `error_user_title`, `error_user_msg` and subcode; the raw error stays in `platformError`.

  - **stage** `string`: Meta ad create failures only. The step that failed:

- `media`: image/video download or upload.
- `campaign`
- `adset`
- `creative`
- `ad`: the ad POST itself, where Meta's code 31 / 3858385 hold and 100 / 1359188 payment rejections land.
- `activation`: switching the created objects on. No longer returned: a refused switch to ACTIVE now answers 201 with an `activation_failed:` warning and the objects kept PAUSED.
- `other`: a read or check before any write.
 - one of: media, campaign, adset, creative, ad, activation, other
  - **adAccountId** `string`: Meta ad create failures only. The ad account the request wrote to (`act_...`).
  - **createdObjects** `array[object]`: Meta ad create failures only. Every object this request created before failing, in creation order. Objects you referenced (an existing campaign, ad set, creative or video) are never listed and never deleted.
    - **type** `string`: No description - one of: campaign, adset, creative, ad, video, image
    - **id** `string`: Meta object id; for `image` the image hash.
    - **cleanup** `string`: - `deleted`: Zernio deleted it.
- `left_behind`: cleanup could not delete it (Meta refused the delete, or the delete hit a network error), so it still exists.
- `kept`: deliberately not deleted. Either an image (hashes are shared by every upload of the same file), or an object Zernio found by name after Meta lost its answer to the create, which may belong to another request with the same name.
 - one of: deleted, left_behind, kept
  - **unconfirmedWrite** `object`: Meta ad create failures only. Set when Meta answered a create with a 5xx or a non-JSON body, or dropped the connection after the request was sent, and reading the parent back by name did not settle it. Zernio did not retry it.

One of these left it open:

- The read-back found several objects of the same name.
- The parent could not be read, or the object is a creative, which has no reliable read-back.
- The read-back found no such object, after a dropped connection, a non-JSON body, a 5xx with `retryOnServerError: false`, or a 5xx on a create whose own body is `status: ACTIVE`.

    - **type** `string`: No description - one of: campaign, adset, creative, ad
    - **parentId** `string`: Where to look for it: the ad set for an ad, the campaign for an ad set, the ad account otherwise.
  - **quotaExhausted** `boolean`: Google Ads and YouTube Analytics 429 only. True when the upstream Google quota is spent rather than a Zernio limit.
  - **quotaScope** `string`: Google Ads and YouTube Analytics 429 only, when the scope is known. DEVELOPER is the shared budget (the developer token for Google Ads, the Google Cloud project for YouTube Analytics); ACCOUNT is your ad account (Google Ads only). - one of: DEVELOPER, ACCOUNT
  - **budgetScope** `string`: Zernio Google Ads burst-limit 429 only (never set alongside `quotaExhausted`). `account` is the limit of 15 requests per minute per connected Google Ads account; `user` is the ceiling of 120 requests per minute per Zernio user across all their Google Ads accounts. Both clear within a minute. - one of: account, user

#### 409: The account exists but is inactive or needs reconnection. Reconnect it, then read GET /v1/accounts for its current account ID before retrying. Code: ads_connection_required.

**Response Body:**

- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **docUrl** `string`: Documentation page for resolving the error, when one applies.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.

- **provider** `string`: Email API: the mailbox provider the error came from (`google`, `microsoft`).
- **providerError** `object`: Email API: the provider's own error code and message, for support and logs. Branch on `code`, not on this.
  - **code** `string,null`: No description
  - **message** `string,null`: No description
- **retryable** `boolean`: Email API: whether the same request can succeed if retried (with the same `Idempotency-Key` for sends).
- **requestId** `string`: Email API: the request id, also in the `X-Request-Id` header. Quote it when you contact support.
- **docsUrl** `string`: Email API: where this `code` is documented.
- **details** `object`: Additional structured context for the error (for example field-level validation errors). What it carries depends on the endpoint and platform.

**Meta ads refusals.** The ones customers hit most come back with their own `code` instead of `platform_api_error`, the field to fix in `param`, and an English `details.hint` saying how to fix it. `details.fix` ({ label, url }) links the Meta page that resolves it, when there is one.

The code is picked from Meta's numeric `code` and `error_subcode`, never from the message, so it is the same whatever language Meta wrote `error_user_msg` in. Branch on `code` and show `details.hint`.

- `ad_account_payment_required`: the ad account has no usable payment method (subcode 1815010, 1359188).
- `ad_account_policy_not_accepted`: a business admin has to accept Meta's non-discrimination policy (subcode 2859002, 2859024).
- `lead_ads_terms_not_accepted`: the Facebook Page has not accepted the Lead Ads terms (subcode 1815089).
- `verified_advertiser_required`: the market requires a verified beneficiary and payer; send `regionalRegulationIdentities` (subcode 3858634, 3858636, 3858442, 3858548, 3858495).
- `ad_account_unusable`: the ad account is not active, or the business is restricted from advertising (subcode 1885316, 2446325, and 2490592, 4841021 below).
- `meta_security_check_required`: the Facebook user behind the connection is held at a security check (code 31).
- `meta_action_blocked`: Meta temporarily blocked the Facebook user from this action (subcode 2859015).
- `meta_ad_consent_required`: the Page or Instagram owner chose Meta without ads or less personalized ads (subcode 3858231, 3858412).
- `instagram_account_restricted`: Meta restricted the Instagram account the ad runs as (subcode 2207070).
- `insufficient_permissions`: the ad account cannot use the Instagram account, or the Facebook user has no advertiser permission on the Page (subcode 1815199, 1815202, 1885499, 1341012, and the Advertise permission case below).
- `whatsapp_number_not_linked`: the WhatsApp number is no longer linked to the Page (subcode 2446880).
- `invalid_targeting`: Meta refused the audience or a placement (subcode 1487079, 1870247, 2909035, 1870189, 1870188, 1487756, 1487843, 2490562).
- `post_not_boostable`: Meta does not allow the post to be promoted (subcode 2446187, 2875030, 1487472).
- `budget_too_low`: the budget is below Meta's minimum, which `platformError.error_user_msg` states (subcode 1885272).
- `meta_object_not_accessible`: Meta cannot load an object the request names (wrong ID, deleted, or no access); `platformError.message` names the ID (subcode 33, on ads routes only).

Meta's own error stays in the message and in `platformError`.

**Private replies.** `privateReplyConsumed` appears on the private-reply endpoint's 400 when the comment's single reply is already spent. `privateReplyOutcome: "unknown"` appears on its 502 when Meta answered a server error and the reply may have been delivered.

**Google quota 429.** On a Google Ads or YouTube Analytics 429, `quotaExhausted: true` marks the failure as Google's own quota, not a Zernio rate limit, so you can keep calling other platforms instead of backing off everywhere. When Google names the scope, `quotaScope` is also set:

- `DEVELOPER`: the shared budget (the developer token for Google Ads, the Google Cloud project for YouTube Analytics). Every account on Zernio is affected and there is nothing to change on your side.
- `ACCOUNT`: your own ad account (Google Ads only).

A Meta 429 carries neither field.

**Zernio Google Ads burst-limit 429.** Carries `budgetScope` instead, never `quotaExhausted`. These are Zernio's own limits, applied before the call reaches Google, and both clear within a minute. They are separate axes from `quotaScope`, not the same pool named twice.

- `account`: the limit of 15 requests per minute per connected Google Ads account.
- `user`: the ceiling of 120 requests per minute per Zernio user across all their Google Ads accounts.

**Failed Meta ad create** (`POST /v1/ads/create`, `POST /v1/ads/boost`, `POST /v1/ads/ctwa`). Carries `stage` (where it failed), `adAccountId` (on which ad account) and `createdObjects` (every object this request had already created, with what cleanup did to it).

- `left_behind` objects still exist on the ad account (Meta refused the delete, typically on a held account, or the delete hit a network error). Delete them yourself or reuse them.
- `unconfirmedWrite` is set when Meta answered a create with a 5xx or a non-JSON body, or dropped the connection, and Zernio could not confirm whether the object exists. Check that parent before creating it again.

**Ineligible ad account: 422 `ad_account_unusable`.** When Meta refuses a write because the ad account's status is ineligible to manage ads (Meta subcode 2490592 or 4841021, on the write itself or on a cleanup delete), any Meta ads write answers it.

That covers create, boost, CTWA, campaign / ad set / ad updates and status changes.

The message names the ad account when known, links Meta's fix (https://www.facebook.com/business/help/422289316306981), lists the objects left behind and ends with Meta's original error, which also stays verbatim in `platformError`.

Meta often reports the first failure under an unrelated code (e.g. 1487194 on the creative), so branch on `code`, not on `platformError`.

**Missing Advertise permission: 403 `insufficient_permissions`.** When the Facebook user behind the connection's token lacks the Advertise permission on the ad account, any Meta ads write answers it.

Triggers: Meta subcode 2490585, 2490591 or 4841020 on the write, or a code 200 refusal such as 1487194 whose cleanup deletes Meta refused with 2490591 or 4841020.

Meta may still accept the campaign and ad set and refuse only the ad. The message names the ad account, asks for the Advertise or Manage permission on it (or a token granted `ads_management`, by reconnecting) and lists the objects left behind.

It ends with Meta's `error_user_title`, `error_user_msg` and subcode; the raw error stays in `platformError`.

  - **stage** `string`: Meta ad create failures only. The step that failed:

- `media`: image/video download or upload.
- `campaign`
- `adset`
- `creative`
- `ad`: the ad POST itself, where Meta's code 31 / 3858385 hold and 100 / 1359188 payment rejections land.
- `activation`: switching the created objects on. No longer returned: a refused switch to ACTIVE now answers 201 with an `activation_failed:` warning and the objects kept PAUSED.
- `other`: a read or check before any write.
 - one of: media, campaign, adset, creative, ad, activation, other
  - **adAccountId** `string`: Meta ad create failures only. The ad account the request wrote to (`act_...`).
  - **createdObjects** `array[object]`: Meta ad create failures only. Every object this request created before failing, in creation order. Objects you referenced (an existing campaign, ad set, creative or video) are never listed and never deleted.
    - **type** `string`: No description - one of: campaign, adset, creative, ad, video, image
    - **id** `string`: Meta object id; for `image` the image hash.
    - **cleanup** `string`: - `deleted`: Zernio deleted it.
- `left_behind`: cleanup could not delete it (Meta refused the delete, or the delete hit a network error), so it still exists.
- `kept`: deliberately not deleted. Either an image (hashes are shared by every upload of the same file), or an object Zernio found by name after Meta lost its answer to the create, which may belong to another request with the same name.
 - one of: deleted, left_behind, kept
  - **unconfirmedWrite** `object`: Meta ad create failures only. Set when Meta answered a create with a 5xx or a non-JSON body, or dropped the connection after the request was sent, and reading the parent back by name did not settle it. Zernio did not retry it.

One of these left it open:

- The read-back found several objects of the same name.
- The parent could not be read, or the object is a creative, which has no reliable read-back.
- The read-back found no such object, after a dropped connection, a non-JSON body, a 5xx with `retryOnServerError: false`, or a 5xx on a create whose own body is `status: ACTIVE`.

    - **type** `string`: No description - one of: campaign, adset, creative, ad
    - **parentId** `string`: Where to look for it: the ad set for an ad, the campaign for an ad set, the ad account otherwise.
  - **quotaExhausted** `boolean`: Google Ads and YouTube Analytics 429 only. True when the upstream Google quota is spent rather than a Zernio limit.
  - **quotaScope** `string`: Google Ads and YouTube Analytics 429 only, when the scope is known. DEVELOPER is the shared budget (the developer token for Google Ads, the Google Cloud project for YouTube Analytics); ACCOUNT is your ad account (Google Ads only). - one of: DEVELOPER, ACCOUNT
  - **budgetScope** `string`: Zernio Google Ads burst-limit 429 only (never set alongside `quotaExhausted`). `account` is the limit of 15 requests per minute per connected Google Ads account; `user` is the ceiling of 120 requests per minute per Zernio user across all their Google Ads accounts. Both clear within a minute. - one of: account, user

#### 429: LinkedIn rate limit hit. Retry with backoff.

---

## POST /v1/ads/conversions/destinations

**Create a conversion destination**

Creates a new place to send conversion events: a Google Ads conversion action or a LinkedIn conversion rule.

Supported for LinkedIn (conversion rule) and Google Ads (conversion action). It returns 405 for:

- Meta and OpenAI Ads: their pixels are created via their own tracking-tags flow instead (`POST /v1/accounts/{accountId}/tracking-tags`)
- Reddit: create Reddit pixels in Reddit Ads Manager

### LinkedIn

Creation is NOT idempotent. A retry creates a second destination. Deduplicate before retrying.

The rule is created with `conversionMethod=CONVERSIONS_API` and (by default) auto-associated with all of the ad account's campaigns via `autoAssociationType=ALL_CAMPAIGNS`. Pass `autoAssociationType: NONE` to opt out and manage associations explicitly via the associations endpoints below.

365-day attribution windows are only valid for these rule types; the API rejects other combinations locally:

- `SUBMIT_APPLICATION`
- `PURCHASE`
- `ADD_TO_CART`
- `QUALIFIED_LEAD`
- `LEAD`

### Google Ads

Calling with a name that already exists reuses the existing conversion action transparently (the response is identical to a fresh create).

Calling with the same name but a different category, or a name already used by a website (`WEBPAGE`) action, returns a typed `IDEMPOTENCY_CONFLICT` (409) rather than silently returning the mismatched action.

The conversion action is created with `type=UPLOAD_CLICKS` (required for API-uploaded offline conversions, immutable after creation).

The `type` field carries the Google `ConversionActionCategory` enum value, e.g. `PURCHASE`, `SUBSCRIBE_PAID`, `SIGNUP`, `IMPORTED_LEAD`, `BOOK_APPOINTMENT`. Unified standard event names (e.g. `Purchase`, `Subscribe`, `CompleteRegistration`, `Lead`, `Schedule`) are resolved to their Google category equivalents automatically.

The action defaults to secondary (non-primary) to avoid immediately steering Smart Bidding; pass `primaryForGoal: true` to opt in.


### Request Body

- **accountId** (required) `string`: The connected ads account (SocialAccount ID).
- **adAccountId** (required) `string`: Ad account ID. For LinkedIn: numeric (e.g. "5123456") or
full `urn:li:sponsoredAccount:{id}` URN. For Google: numeric
customer ID (e.g. "1234567890") or `customers/{id}` form.

- **name** (required) `string`: No description
- **type** (required) `string`: Conversion type. Unknown values pass through to the platform.

- LinkedIn: a unified standard event name (e.g. "Purchase", "Lead", "AddToCart") or a LinkedIn rule type enum (e.g. "PURCHASE", "QUALIFIED_LEAD").
- Google: a unified standard event name (Purchase, Subscribe, CompleteRegistration, Lead, Schedule) or a Google ConversionActionCategory enum value directly (e.g. "PURCHASE", "SUBSCRIBE_PAID", "SIGNUP", "IMPORTED_LEAD", "BOOK_APPOINTMENT").

- **attributionType** `string`: LinkedIn only. - one of: LAST_TOUCH_BY_CAMPAIGN, LAST_TOUCH_BY_CONVERSION
- **postClickAttributionWindowSize** `integer`: LinkedIn only. Default 30. 365 only allowed for LEAD,
PURCHASE, ADD_TO_CART, QUALIFIED_LEAD, SUBMIT_APPLICATION
rule types; the API rejects other combinations locally.
 - one of: 1, 7, 30, 90, 365
- **viewThroughAttributionWindowSize** `integer`: LinkedIn only. Default 7. Same 365-day-window type
restriction applies as `postClickAttributionWindowSize`.
 - one of: 1, 7, 30, 90, 365
- **valueType** `string`: LinkedIn only. DYNAMIC (default) uses the per-event `value`
from `sendConversions`. FIXED uses the rule's `value` field.
NO_VALUE drops monetary value entirely.
 - one of: DYNAMIC, FIXED, NO_VALUE
- **value** `object`: LinkedIn only. Static conversion value. Used when
`valueType=FIXED`. The currency should match the ad
account's currency.

- **autoAssociationType** `string`: LinkedIn only. Controls campaign association at rule-creation time:

- ALL_CAMPAIGNS: associate the rule with every active, paused, and draft campaign in the ad account.
- OBJECTIVE_BASED: associate only campaigns whose objective matches the rule's type.
- NONE: don't auto-associate. Manage associations via the `/associations` endpoints below.

Auto-association runs once at create time; campaigns added after the rule still need explicit association.
 - one of: ALL_CAMPAIGNS, OBJECTIVE_BASED, NONE
- **countingType** `string`: Google Ads only. Whether to count multiple conversions from
the same click (MANY_PER_CLICK) or at most one
(ONE_PER_CLICK). Defaults to MANY_PER_CLICK if omitted.
 - one of: MANY_PER_CLICK, ONE_PER_CLICK
- **primaryForGoal** `boolean`: Google Ads only. When true, the conversion action is marked
as primary and immediately influences Smart Bidding. Defaults
to false (secondary, record-only) to avoid unintentionally
steering the customer's campaigns on creation.


### Responses

#### 201: Destination created

**Response Body:**

- **platform** `string`: No description - one of: linkedinads, googleads
- **destination**: `ConversionDestination` - See schema definition

#### 400: Invalid body or platform validation failure.

#### 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.

**Response Body:**

- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **docUrl** `string`: Documentation page for resolving the error, when one applies.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.

- **provider** `string`: Email API: the mailbox provider the error came from (`google`, `microsoft`).
- **providerError** `object`: Email API: the provider's own error code and message, for support and logs. Branch on `code`, not on this.
  - **code** `string,null`: No description
  - **message** `string,null`: No description
- **retryable** `boolean`: Email API: whether the same request can succeed if retried (with the same `Idempotency-Key` for sends).
- **requestId** `string`: Email API: the request id, also in the `X-Request-Id` header. Quote it when you contact support.
- **docsUrl** `string`: Email API: where this `code` is documented.
- **details** `object`: Additional structured context for the error (for example field-level validation errors). What it carries depends on the endpoint and platform.

**Meta ads refusals.** The ones customers hit most come back with their own `code` instead of `platform_api_error`, the field to fix in `param`, and an English `details.hint` saying how to fix it. `details.fix` ({ label, url }) links the Meta page that resolves it, when there is one.

The code is picked from Meta's numeric `code` and `error_subcode`, never from the message, so it is the same whatever language Meta wrote `error_user_msg` in. Branch on `code` and show `details.hint`.

- `ad_account_payment_required`: the ad account has no usable payment method (subcode 1815010, 1359188).
- `ad_account_policy_not_accepted`: a business admin has to accept Meta's non-discrimination policy (subcode 2859002, 2859024).
- `lead_ads_terms_not_accepted`: the Facebook Page has not accepted the Lead Ads terms (subcode 1815089).
- `verified_advertiser_required`: the market requires a verified beneficiary and payer; send `regionalRegulationIdentities` (subcode 3858634, 3858636, 3858442, 3858548, 3858495).
- `ad_account_unusable`: the ad account is not active, or the business is restricted from advertising (subcode 1885316, 2446325, and 2490592, 4841021 below).
- `meta_security_check_required`: the Facebook user behind the connection is held at a security check (code 31).
- `meta_action_blocked`: Meta temporarily blocked the Facebook user from this action (subcode 2859015).
- `meta_ad_consent_required`: the Page or Instagram owner chose Meta without ads or less personalized ads (subcode 3858231, 3858412).
- `instagram_account_restricted`: Meta restricted the Instagram account the ad runs as (subcode 2207070).
- `insufficient_permissions`: the ad account cannot use the Instagram account, or the Facebook user has no advertiser permission on the Page (subcode 1815199, 1815202, 1885499, 1341012, and the Advertise permission case below).
- `whatsapp_number_not_linked`: the WhatsApp number is no longer linked to the Page (subcode 2446880).
- `invalid_targeting`: Meta refused the audience or a placement (subcode 1487079, 1870247, 2909035, 1870189, 1870188, 1487756, 1487843, 2490562).
- `post_not_boostable`: Meta does not allow the post to be promoted (subcode 2446187, 2875030, 1487472).
- `budget_too_low`: the budget is below Meta's minimum, which `platformError.error_user_msg` states (subcode 1885272).
- `meta_object_not_accessible`: Meta cannot load an object the request names (wrong ID, deleted, or no access); `platformError.message` names the ID (subcode 33, on ads routes only).

Meta's own error stays in the message and in `platformError`.

**Private replies.** `privateReplyConsumed` appears on the private-reply endpoint's 400 when the comment's single reply is already spent. `privateReplyOutcome: "unknown"` appears on its 502 when Meta answered a server error and the reply may have been delivered.

**Google quota 429.** On a Google Ads or YouTube Analytics 429, `quotaExhausted: true` marks the failure as Google's own quota, not a Zernio rate limit, so you can keep calling other platforms instead of backing off everywhere. When Google names the scope, `quotaScope` is also set:

- `DEVELOPER`: the shared budget (the developer token for Google Ads, the Google Cloud project for YouTube Analytics). Every account on Zernio is affected and there is nothing to change on your side.
- `ACCOUNT`: your own ad account (Google Ads only).

A Meta 429 carries neither field.

**Zernio Google Ads burst-limit 429.** Carries `budgetScope` instead, never `quotaExhausted`. These are Zernio's own limits, applied before the call reaches Google, and both clear within a minute. They are separate axes from `quotaScope`, not the same pool named twice.

- `account`: the limit of 15 requests per minute per connected Google Ads account.
- `user`: the ceiling of 120 requests per minute per Zernio user across all their Google Ads accounts.

**Failed Meta ad create** (`POST /v1/ads/create`, `POST /v1/ads/boost`, `POST /v1/ads/ctwa`). Carries `stage` (where it failed), `adAccountId` (on which ad account) and `createdObjects` (every object this request had already created, with what cleanup did to it).

- `left_behind` objects still exist on the ad account (Meta refused the delete, typically on a held account, or the delete hit a network error). Delete them yourself or reuse them.
- `unconfirmedWrite` is set when Meta answered a create with a 5xx or a non-JSON body, or dropped the connection, and Zernio could not confirm whether the object exists. Check that parent before creating it again.

**Ineligible ad account: 422 `ad_account_unusable`.** When Meta refuses a write because the ad account's status is ineligible to manage ads (Meta subcode 2490592 or 4841021, on the write itself or on a cleanup delete), any Meta ads write answers it.

That covers create, boost, CTWA, campaign / ad set / ad updates and status changes.

The message names the ad account when known, links Meta's fix (https://www.facebook.com/business/help/422289316306981), lists the objects left behind and ends with Meta's original error, which also stays verbatim in `platformError`.

Meta often reports the first failure under an unrelated code (e.g. 1487194 on the creative), so branch on `code`, not on `platformError`.

**Missing Advertise permission: 403 `insufficient_permissions`.** When the Facebook user behind the connection's token lacks the Advertise permission on the ad account, any Meta ads write answers it.

Triggers: Meta subcode 2490585, 2490591 or 4841020 on the write, or a code 200 refusal such as 1487194 whose cleanup deletes Meta refused with 2490591 or 4841020.

Meta may still accept the campaign and ad set and refuse only the ad. The message names the ad account, asks for the Advertise or Manage permission on it (or a token granted `ads_management`, by reconnecting) and lists the objects left behind.

It ends with Meta's `error_user_title`, `error_user_msg` and subcode; the raw error stays in `platformError`.

  - **stage** `string`: Meta ad create failures only. The step that failed:

- `media`: image/video download or upload.
- `campaign`
- `adset`
- `creative`
- `ad`: the ad POST itself, where Meta's code 31 / 3858385 hold and 100 / 1359188 payment rejections land.
- `activation`: switching the created objects on. No longer returned: a refused switch to ACTIVE now answers 201 with an `activation_failed:` warning and the objects kept PAUSED.
- `other`: a read or check before any write.
 - one of: media, campaign, adset, creative, ad, activation, other
  - **adAccountId** `string`: Meta ad create failures only. The ad account the request wrote to (`act_...`).
  - **createdObjects** `array[object]`: Meta ad create failures only. Every object this request created before failing, in creation order. Objects you referenced (an existing campaign, ad set, creative or video) are never listed and never deleted.
    - **type** `string`: No description - one of: campaign, adset, creative, ad, video, image
    - **id** `string`: Meta object id; for `image` the image hash.
    - **cleanup** `string`: - `deleted`: Zernio deleted it.
- `left_behind`: cleanup could not delete it (Meta refused the delete, or the delete hit a network error), so it still exists.
- `kept`: deliberately not deleted. Either an image (hashes are shared by every upload of the same file), or an object Zernio found by name after Meta lost its answer to the create, which may belong to another request with the same name.
 - one of: deleted, left_behind, kept
  - **unconfirmedWrite** `object`: Meta ad create failures only. Set when Meta answered a create with a 5xx or a non-JSON body, or dropped the connection after the request was sent, and reading the parent back by name did not settle it. Zernio did not retry it.

One of these left it open:

- The read-back found several objects of the same name.
- The parent could not be read, or the object is a creative, which has no reliable read-back.
- The read-back found no such object, after a dropped connection, a non-JSON body, a 5xx with `retryOnServerError: false`, or a 5xx on a create whose own body is `status: ACTIVE`.

    - **type** `string`: No description - one of: campaign, adset, creative, ad
    - **parentId** `string`: Where to look for it: the ad set for an ad, the campaign for an ad set, the ad account otherwise.
  - **quotaExhausted** `boolean`: Google Ads and YouTube Analytics 429 only. True when the upstream Google quota is spent rather than a Zernio limit.
  - **quotaScope** `string`: Google Ads and YouTube Analytics 429 only, when the scope is known. DEVELOPER is the shared budget (the developer token for Google Ads, the Google Cloud project for YouTube Analytics); ACCOUNT is your ad account (Google Ads only). - one of: DEVELOPER, ACCOUNT
  - **budgetScope** `string`: Zernio Google Ads burst-limit 429 only (never set alongside `quotaExhausted`). `account` is the limit of 15 requests per minute per connected Google Ads account; `user` is the ceiling of 120 requests per minute per Zernio user across all their Google Ads accounts. Both clear within a minute. - one of: account, user

#### 403: Ads access required (Ads add-on on legacy plans, included on usage-based plans), or the connected LinkedIn account lacks the `rw_conversions` scope (reconnect required).

Also returned with code `ad_account_not_selected` when the connection has selected ad accounts and the targeted ad account is not among them. `details` carries `accountId`, `adAccountId` and `selectedAdAccountIds`.

Fix: add the ad account to the selection (GET /v1/connect/{platform}/ads with adAccountIds, which replaces the selection) or use a connection that has it selected.


#### 404: The account or requested resource was not found or is not accessible. An account ID may have been disconnected and removed. Read GET /v1/accounts for current account IDs.

**Response Body:**

- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **docUrl** `string`: Documentation page for resolving the error, when one applies.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.

- **provider** `string`: Email API: the mailbox provider the error came from (`google`, `microsoft`).
- **providerError** `object`: Email API: the provider's own error code and message, for support and logs. Branch on `code`, not on this.
  - **code** `string,null`: No description
  - **message** `string,null`: No description
- **retryable** `boolean`: Email API: whether the same request can succeed if retried (with the same `Idempotency-Key` for sends).
- **requestId** `string`: Email API: the request id, also in the `X-Request-Id` header. Quote it when you contact support.
- **docsUrl** `string`: Email API: where this `code` is documented.
- **details** `object`: Additional structured context for the error (for example field-level validation errors). What it carries depends on the endpoint and platform.

**Meta ads refusals.** The ones customers hit most come back with their own `code` instead of `platform_api_error`, the field to fix in `param`, and an English `details.hint` saying how to fix it. `details.fix` ({ label, url }) links the Meta page that resolves it, when there is one.

The code is picked from Meta's numeric `code` and `error_subcode`, never from the message, so it is the same whatever language Meta wrote `error_user_msg` in. Branch on `code` and show `details.hint`.

- `ad_account_payment_required`: the ad account has no usable payment method (subcode 1815010, 1359188).
- `ad_account_policy_not_accepted`: a business admin has to accept Meta's non-discrimination policy (subcode 2859002, 2859024).
- `lead_ads_terms_not_accepted`: the Facebook Page has not accepted the Lead Ads terms (subcode 1815089).
- `verified_advertiser_required`: the market requires a verified beneficiary and payer; send `regionalRegulationIdentities` (subcode 3858634, 3858636, 3858442, 3858548, 3858495).
- `ad_account_unusable`: the ad account is not active, or the business is restricted from advertising (subcode 1885316, 2446325, and 2490592, 4841021 below).
- `meta_security_check_required`: the Facebook user behind the connection is held at a security check (code 31).
- `meta_action_blocked`: Meta temporarily blocked the Facebook user from this action (subcode 2859015).
- `meta_ad_consent_required`: the Page or Instagram owner chose Meta without ads or less personalized ads (subcode 3858231, 3858412).
- `instagram_account_restricted`: Meta restricted the Instagram account the ad runs as (subcode 2207070).
- `insufficient_permissions`: the ad account cannot use the Instagram account, or the Facebook user has no advertiser permission on the Page (subcode 1815199, 1815202, 1885499, 1341012, and the Advertise permission case below).
- `whatsapp_number_not_linked`: the WhatsApp number is no longer linked to the Page (subcode 2446880).
- `invalid_targeting`: Meta refused the audience or a placement (subcode 1487079, 1870247, 2909035, 1870189, 1870188, 1487756, 1487843, 2490562).
- `post_not_boostable`: Meta does not allow the post to be promoted (subcode 2446187, 2875030, 1487472).
- `budget_too_low`: the budget is below Meta's minimum, which `platformError.error_user_msg` states (subcode 1885272).
- `meta_object_not_accessible`: Meta cannot load an object the request names (wrong ID, deleted, or no access); `platformError.message` names the ID (subcode 33, on ads routes only).

Meta's own error stays in the message and in `platformError`.

**Private replies.** `privateReplyConsumed` appears on the private-reply endpoint's 400 when the comment's single reply is already spent. `privateReplyOutcome: "unknown"` appears on its 502 when Meta answered a server error and the reply may have been delivered.

**Google quota 429.** On a Google Ads or YouTube Analytics 429, `quotaExhausted: true` marks the failure as Google's own quota, not a Zernio rate limit, so you can keep calling other platforms instead of backing off everywhere. When Google names the scope, `quotaScope` is also set:

- `DEVELOPER`: the shared budget (the developer token for Google Ads, the Google Cloud project for YouTube Analytics). Every account on Zernio is affected and there is nothing to change on your side.
- `ACCOUNT`: your own ad account (Google Ads only).

A Meta 429 carries neither field.

**Zernio Google Ads burst-limit 429.** Carries `budgetScope` instead, never `quotaExhausted`. These are Zernio's own limits, applied before the call reaches Google, and both clear within a minute. They are separate axes from `quotaScope`, not the same pool named twice.

- `account`: the limit of 15 requests per minute per connected Google Ads account.
- `user`: the ceiling of 120 requests per minute per Zernio user across all their Google Ads accounts.

**Failed Meta ad create** (`POST /v1/ads/create`, `POST /v1/ads/boost`, `POST /v1/ads/ctwa`). Carries `stage` (where it failed), `adAccountId` (on which ad account) and `createdObjects` (every object this request had already created, with what cleanup did to it).

- `left_behind` objects still exist on the ad account (Meta refused the delete, typically on a held account, or the delete hit a network error). Delete them yourself or reuse them.
- `unconfirmedWrite` is set when Meta answered a create with a 5xx or a non-JSON body, or dropped the connection, and Zernio could not confirm whether the object exists. Check that parent before creating it again.

**Ineligible ad account: 422 `ad_account_unusable`.** When Meta refuses a write because the ad account's status is ineligible to manage ads (Meta subcode 2490592 or 4841021, on the write itself or on a cleanup delete), any Meta ads write answers it.

That covers create, boost, CTWA, campaign / ad set / ad updates and status changes.

The message names the ad account when known, links Meta's fix (https://www.facebook.com/business/help/422289316306981), lists the objects left behind and ends with Meta's original error, which also stays verbatim in `platformError`.

Meta often reports the first failure under an unrelated code (e.g. 1487194 on the creative), so branch on `code`, not on `platformError`.

**Missing Advertise permission: 403 `insufficient_permissions`.** When the Facebook user behind the connection's token lacks the Advertise permission on the ad account, any Meta ads write answers it.

Triggers: Meta subcode 2490585, 2490591 or 4841020 on the write, or a code 200 refusal such as 1487194 whose cleanup deletes Meta refused with 2490591 or 4841020.

Meta may still accept the campaign and ad set and refuse only the ad. The message names the ad account, asks for the Advertise or Manage permission on it (or a token granted `ads_management`, by reconnecting) and lists the objects left behind.

It ends with Meta's `error_user_title`, `error_user_msg` and subcode; the raw error stays in `platformError`.

  - **stage** `string`: Meta ad create failures only. The step that failed:

- `media`: image/video download or upload.
- `campaign`
- `adset`
- `creative`
- `ad`: the ad POST itself, where Meta's code 31 / 3858385 hold and 100 / 1359188 payment rejections land.
- `activation`: switching the created objects on. No longer returned: a refused switch to ACTIVE now answers 201 with an `activation_failed:` warning and the objects kept PAUSED.
- `other`: a read or check before any write.
 - one of: media, campaign, adset, creative, ad, activation, other
  - **adAccountId** `string`: Meta ad create failures only. The ad account the request wrote to (`act_...`).
  - **createdObjects** `array[object]`: Meta ad create failures only. Every object this request created before failing, in creation order. Objects you referenced (an existing campaign, ad set, creative or video) are never listed and never deleted.
    - **type** `string`: No description - one of: campaign, adset, creative, ad, video, image
    - **id** `string`: Meta object id; for `image` the image hash.
    - **cleanup** `string`: - `deleted`: Zernio deleted it.
- `left_behind`: cleanup could not delete it (Meta refused the delete, or the delete hit a network error), so it still exists.
- `kept`: deliberately not deleted. Either an image (hashes are shared by every upload of the same file), or an object Zernio found by name after Meta lost its answer to the create, which may belong to another request with the same name.
 - one of: deleted, left_behind, kept
  - **unconfirmedWrite** `object`: Meta ad create failures only. Set when Meta answered a create with a 5xx or a non-JSON body, or dropped the connection after the request was sent, and reading the parent back by name did not settle it. Zernio did not retry it.

One of these left it open:

- The read-back found several objects of the same name.
- The parent could not be read, or the object is a creative, which has no reliable read-back.
- The read-back found no such object, after a dropped connection, a non-JSON body, a 5xx with `retryOnServerError: false`, or a 5xx on a create whose own body is `status: ACTIVE`.

    - **type** `string`: No description - one of: campaign, adset, creative, ad
    - **parentId** `string`: Where to look for it: the ad set for an ad, the campaign for an ad set, the ad account otherwise.
  - **quotaExhausted** `boolean`: Google Ads and YouTube Analytics 429 only. True when the upstream Google quota is spent rather than a Zernio limit.
  - **quotaScope** `string`: Google Ads and YouTube Analytics 429 only, when the scope is known. DEVELOPER is the shared budget (the developer token for Google Ads, the Google Cloud project for YouTube Analytics); ACCOUNT is your ad account (Google Ads only). - one of: DEVELOPER, ACCOUNT
  - **budgetScope** `string`: Zernio Google Ads burst-limit 429 only (never set alongside `quotaExhausted`). `account` is the limit of 15 requests per minute per connected Google Ads account; `user` is the ceiling of 120 requests per minute per Zernio user across all their Google Ads accounts. Both clear within a minute. - one of: account, user

#### 405: Platform does not support destination creation.

#### 409: Conflict. One of:

- `IDEMPOTENCY_CONFLICT` (Google Ads only): a conversion action with the given name already exists but has a different category. Use a different name or the existing destination.
- ads_connection_required: the account is inactive or needs reconnection. Reconnect it and read GET /v1/accounts for its current ID before retrying.


#### 422: Google Ads connection not usable.

- Code `ads_connection_required`: no Google Ads customer account is left on this connection after its ad-account selection. Reconnect via GET /v1/connect/googleads/ads with a Google user that can reach the ad account, or select one of its ad accounts.
- Code `invalid_resource_state`: the connection reaches only a Google Ads manager account with no client accounts yet. Create one with POST /v1/ads/accounts or under the manager in Google Ads, then retry.


#### 429: Rate limit hit. Retry with backoff.

---


# Related Schema Definitions

## ErrorResponse

Canonical error envelope. `error` is the human-readable message; `type`,
`code`, `param`, `platform`, and `platformError` are top-level siblings
for programmatic handling. For upstream platform failures (`type:
platform_error`), `platformError` carries the provider's raw payload
verbatim (for Meta: `error_subcode`, `error_user_title`, `error_user_msg`).


### Properties

- **error** `string`: Human-readable error message.
- **type** `string`: Error class for programmatic handling. - one of: invalid_request_error, authentication_error, permission_error, not_found, rate_limit_error, platform_error, api_error
- **code** `string`: Stable machine-readable error code.
- **param** `string`: The request field that caused the error, when applicable.
- **docUrl** `string`: Documentation page for resolving the error, when one applies.
- **platform** `string`: Upstream platform (e.g. meta, google, tiktok), present when type is platform_error.
- **platformError** `object`: Raw error payload from the upstream platform, passed through verbatim so
integrators can read provider-specific codes. For Meta this includes
error_subcode, error_user_title, and error_user_msg.

- **provider** `string`: Email API: the mailbox provider the error came from (`google`, `microsoft`).
- **providerError** `object`: Email API: the provider's own error code and message, for support and logs. Branch on `code`, not on this.
  - **code** `string,null`: 
  - **message** `string,null`: 
- **retryable** `boolean`: Email API: whether the same request can succeed if retried (with the same `Idempotency-Key` for sends).
- **requestId** `string`: Email API: the request id, also in the `X-Request-Id` header. Quote it when you contact support.
- **docsUrl** `string`: Email API: where this `code` is documented.
- **details** `object`: Additional structured context for the error (for example field-level validation errors). What it carries depends on the endpoint and platform.

**Meta ads refusals.** The ones customers hit most come back with their own `code` instead of `platform_api_error`, the field to fix in `param`, and an English `details.hint` saying how to fix it. `details.fix` ({ label, url }) links the Meta page that resolves it, when there is one.

The code is picked from Meta's numeric `code` and `error_subcode`, never from the message, so it is the same whatever language Meta wrote `error_user_msg` in. Branch on `code` and show `details.hint`.

- `ad_account_payment_required`: the ad account has no usable payment method (subcode 1815010, 1359188).
- `ad_account_policy_not_accepted`: a business admin has to accept Meta's non-discrimination policy (subcode 2859002, 2859024).
- `lead_ads_terms_not_accepted`: the Facebook Page has not accepted the Lead Ads terms (subcode 1815089).
- `verified_advertiser_required`: the market requires a verified beneficiary and payer; send `regionalRegulationIdentities` (subcode 3858634, 3858636, 3858442, 3858548, 3858495).
- `ad_account_unusable`: the ad account is not active, or the business is restricted from advertising (subcode 1885316, 2446325, and 2490592, 4841021 below).
- `meta_security_check_required`: the Facebook user behind the connection is held at a security check (code 31).
- `meta_action_blocked`: Meta temporarily blocked the Facebook user from this action (subcode 2859015).
- `meta_ad_consent_required`: the Page or Instagram owner chose Meta without ads or less personalized ads (subcode 3858231, 3858412).
- `instagram_account_restricted`: Meta restricted the Instagram account the ad runs as (subcode 2207070).
- `insufficient_permissions`: the ad account cannot use the Instagram account, or the Facebook user has no advertiser permission on the Page (subcode 1815199, 1815202, 1885499, 1341012, and the Advertise permission case below).
- `whatsapp_number_not_linked`: the WhatsApp number is no longer linked to the Page (subcode 2446880).
- `invalid_targeting`: Meta refused the audience or a placement (subcode 1487079, 1870247, 2909035, 1870189, 1870188, 1487756, 1487843, 2490562).
- `post_not_boostable`: Meta does not allow the post to be promoted (subcode 2446187, 2875030, 1487472).
- `budget_too_low`: the budget is below Meta's minimum, which `platformError.error_user_msg` states (subcode 1885272).
- `meta_object_not_accessible`: Meta cannot load an object the request names (wrong ID, deleted, or no access); `platformError.message` names the ID (subcode 33, on ads routes only).

Meta's own error stays in the message and in `platformError`.

**Private replies.** `privateReplyConsumed` appears on the private-reply endpoint's 400 when the comment's single reply is already spent. `privateReplyOutcome: "unknown"` appears on its 502 when Meta answered a server error and the reply may have been delivered.

**Google quota 429.** On a Google Ads or YouTube Analytics 429, `quotaExhausted: true` marks the failure as Google's own quota, not a Zernio rate limit, so you can keep calling other platforms instead of backing off everywhere. When Google names the scope, `quotaScope` is also set:

- `DEVELOPER`: the shared budget (the developer token for Google Ads, the Google Cloud project for YouTube Analytics). Every account on Zernio is affected and there is nothing to change on your side.
- `ACCOUNT`: your own ad account (Google Ads only).

A Meta 429 carries neither field.

**Zernio Google Ads burst-limit 429.** Carries `budgetScope` instead, never `quotaExhausted`. These are Zernio's own limits, applied before the call reaches Google, and both clear within a minute. They are separate axes from `quotaScope`, not the same pool named twice.

- `account`: the limit of 15 requests per minute per connected Google Ads account.
- `user`: the ceiling of 120 requests per minute per Zernio user across all their Google Ads accounts.

**Failed Meta ad create** (`POST /v1/ads/create`, `POST /v1/ads/boost`, `POST /v1/ads/ctwa`). Carries `stage` (where it failed), `adAccountId` (on which ad account) and `createdObjects` (every object this request had already created, with what cleanup did to it).

- `left_behind` objects still exist on the ad account (Meta refused the delete, typically on a held account, or the delete hit a network error). Delete them yourself or reuse them.
- `unconfirmedWrite` is set when Meta answered a create with a 5xx or a non-JSON body, or dropped the connection, and Zernio could not confirm whether the object exists. Check that parent before creating it again.

**Ineligible ad account: 422 `ad_account_unusable`.** When Meta refuses a write because the ad account's status is ineligible to manage ads (Meta subcode 2490592 or 4841021, on the write itself or on a cleanup delete), any Meta ads write answers it.

That covers create, boost, CTWA, campaign / ad set / ad updates and status changes.

The message names the ad account when known, links Meta's fix (https://www.facebook.com/business/help/422289316306981), lists the objects left behind and ends with Meta's original error, which also stays verbatim in `platformError`.

Meta often reports the first failure under an unrelated code (e.g. 1487194 on the creative), so branch on `code`, not on `platformError`.

**Missing Advertise permission: 403 `insufficient_permissions`.** When the Facebook user behind the connection's token lacks the Advertise permission on the ad account, any Meta ads write answers it.

Triggers: Meta subcode 2490585, 2490591 or 4841020 on the write, or a code 200 refusal such as 1487194 whose cleanup deletes Meta refused with 2490591 or 4841020.

Meta may still accept the campaign and ad set and refuse only the ad. The message names the ad account, asks for the Advertise or Manage permission on it (or a token granted `ads_management`, by reconnecting) and lists the objects left behind.

It ends with Meta's `error_user_title`, `error_user_msg` and subcode; the raw error stays in `platformError`.

  - **stage** `string`: Meta ad create failures only. The step that failed:

- `media`: image/video download or upload.
- `campaign`
- `adset`
- `creative`
- `ad`: the ad POST itself, where Meta's code 31 / 3858385 hold and 100 / 1359188 payment rejections land.
- `activation`: switching the created objects on. No longer returned: a refused switch to ACTIVE now answers 201 with an `activation_failed:` warning and the objects kept PAUSED.
- `other`: a read or check before any write.
 - one of: media, campaign, adset, creative, ad, activation, other
  - **adAccountId** `string`: Meta ad create failures only. The ad account the request wrote to (`act_...`).
  - **createdObjects** `array`: Meta ad create failures only. Every object this request created before failing, in creation order. Objects you referenced (an existing campaign, ad set, creative or video) are never listed and never deleted.
  - **unconfirmedWrite** `object`: Meta ad create failures only. Set when Meta answered a create with a 5xx or a non-JSON body, or dropped the connection after the request was sent, and reading the parent back by name did not settle it. Zernio did not retry it.

One of these left it open:

- The read-back found several objects of the same name.
- The parent could not be read, or the object is a creative, which has no reliable read-back.
- The read-back found no such object, after a dropped connection, a non-JSON body, a 5xx with `retryOnServerError: false`, or a 5xx on a create whose own body is `status: ACTIVE`.

  - **quotaExhausted** `boolean`: Google Ads and YouTube Analytics 429 only. True when the upstream Google quota is spent rather than a Zernio limit.
  - **quotaScope** `string`: Google Ads and YouTube Analytics 429 only, when the scope is known. DEVELOPER is the shared budget (the developer token for Google Ads, the Google Cloud project for YouTube Analytics); ACCOUNT is your ad account (Google Ads only). - one of: DEVELOPER, ACCOUNT
  - **budgetScope** `string`: Zernio Google Ads burst-limit 429 only (never set alongside `quotaExhausted`). `account` is the limit of 15 requests per minute per connected Google Ads account; `user` is the ceiling of 120 requests per minute per Zernio user across all their Google Ads accounts. Both clear within a minute. - one of: account, user

## ConversionDestination

A discoverable conversion destination on an ad platform: a Meta pixel,
Google conversion action, or LinkedIn conversion rule. Returned by
`listConversionDestinations`, `getConversionDestination`,
`createConversionDestination`, and `updateConversionDestination`.


### Properties

- **id** (required) `string`: Platform-native identifier. Pass back as `destinationId` on event
send and as the path segment on CRUD endpoints.

- **name** (required) `string`: No description
- **type** `string`: Present when the platform locks the event type/category to the
destination (Google conversion actions, LinkedIn conversion rules).
Absent for Meta pixels (which accept any event name per request).

- **status** `string`: For LinkedIn, `inactive` means the rule is soft-deleted (`enabled: false`).
 - one of: active, inactive
- **adAccountId** `string`: Set by adapters whose destinations are scoped to a specific ad
account (LinkedIn). Pass back on subsequent CRUD calls to
identify the parent ad account.