# List automation logs API Reference

Paginated list of every comment that triggered this automation, with send status and commenter info.

## GET /v1/comment-automations/{automationId}/logs

**List automation logs**

Paginated list of every comment that triggered this automation, with send status and commenter info.

### Parameters

- **automationId** (required) in path: No description
- **status** (optional) in query: Filter by result status
- **limit** (optional) in query: No description
- **skip** (optional) in query: No description

### Responses

#### 200: Trigger logs with pagination

**Response Body:**

- **success** `boolean`: No description
- **logs** `array[CommentAutomationLog]`: 
- **pagination** `object`: 
  - **total** `integer`: No description
  - **limit** `integer`: No description
  - **skip** `integer`: No description
  - **hasMore** `boolean`: No description
- **misses** `object`: Comments that reached this automation but matched none of its keywords. These produce no log entry, so this is the only signal that a keyword is catching nothing. Retained for a short window, then dropped.
  - **total** `integer`: Number of non-matching comments in the retention window
  - **retentionDays** `integer`: How many days of non-matching comments the total covers
  - **samples** `array[object]`: A few of the most recent non-matching comments, for diagnosing a keyword setup.
    - **commentText** `string`: No description
    - **commenterName** `string`: No description
    - **excludedBy** `string`: Set when an exclusion keyword vetoed an otherwise matching comment
    - **at** `string` (date-time): No description

#### 400: Invalid request

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

- **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 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 (image hashes are shared by every upload of the same file). - one of: deleted, left_behind, kept
  - **unconfirmedWrite** `object`: Meta ad create failures only, when Meta answered a create with a 5xx or dropped the connection and the object could not be looked up. Zernio did not retry it.
    - **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

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

- **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 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 (image hashes are shared by every upload of the same file). - one of: deleted, left_behind, kept
  - **unconfirmedWrite** `object`: Meta ad create failures only, when Meta answered a create with a 5xx or dropped the connection and the object could not be looked up. Zernio did not retry it.
    - **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

#### 404: Resource not found

**Response Body:**

- **error** `string`: No description (example: "Not found")

---


# Related Schema Definitions

## CommentAutomationLog

### Properties

- **id** `string`: No description
- **commentId** `string`: No description
- **commenterId** `string`: No description
- **commenterName** `string,null`: No description
- **commenterUsername** `string,null`: No description
- **commentText** `string,null`: No description
- **source** `string,null`: Which door triggered this send. Null on rows written before this field existed (all of those are comment-triggered). - one of: comment, live_comment, story_reply, story_mention, dm, 
- **status** `string`: DM outcome:

- 'pending': the automation has a dmDelaySeconds and the response is queued but not sent yet.
- 'gated': the follow-gate confirmation DM went out and we are waiting for the tap. It flips to 'sent' or 'skipped' when they tap.
- 'skipped': also covers repeatPolicy, cooldown and dedupeSameTextHours suppressions, with the reason in error. - one of: pending, sent, failed, skipped, gated
- **audienceOutcome** `string,null`: How the audience rule resolved. Null on automations without one. - one of: passed, blocked, gate_sent, gate_passed, gate_failed, 
- **gateButtonStatus** `string,null`: Whether the follow-gate button reached the commenter: 'rejected' = Meta refused the gate DM, 'omitted' = the prompt went out as plain text because it was over 640 characters. Null when no gate DM was sent. - one of: delivered, rejected, omitted, 
- **commenterIsFollower** `boolean,null`: Follow relationship at decision time. Null when Instagram would not tell us (the commenter never messaged the account).
- **commenterFollowerCount** `integer,null`: No description
- **gateResolvedAt** `string,null`: When the follow-gate tap was claimed.
- **error** `string,null`: DM error message when status is failed, or the reason when it is skipped.
- **platformError** `object,null`: Platform error codes of the failed DM (Meta `code` and `error_subcode`), when the platform sent them. Null on successful rows and on rows written before this field existed.
- **privateReplyConsumed** `boolean,null`: True when the failed send spent the comment's single private reply (Instagram subcode 1545133 or 2534023, or Meta code 10900 on Instagram and Facebook), the same rule as `details.privateReplyConsumed` on the private-reply endpoint. Null on direct DMs and on rows written before this field existed.
- **commentReplyStatus** `string,null`: Outcome of the optional public reply on the triggering comment. With publicReplyPolicy after_dm, 'skipped' if no commentReply was configured or if the DM failed (the public reply is not attempted in that case). - one of: pending, sent, failed, skipped, 
- **commentReplyError** `string,null`: Public-reply error message if commentReplyStatus is failed
- **publicReplyPostedAt** `string,null`: When the public reply was posted. Null when it was not.
- **likeSkipped** `string,null`: Why actions.likeComment did not like the comment. Null when it did or was not configured.
- **hideSkipped** `string,null`: Why actions.hideComment did not hide the comment. Null when it did or was not configured.
- **mediaError** `string,null`: Why the dmMedia follow-up was not delivered. The DM itself still counts as sent.
- **nextDueAt** `string,null`: When the next queued send fires. Present only while something is still pending.
- **clickedAt** `string,null`: This recipient's first click on a tracked link (what uniqueClicks counts).
- **clickCount** `integer`: This recipient's total clicks on tracked links.
- **createdAt** `string`: No description

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

- **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 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, when Meta answered a create with a 5xx or dropped the connection and the object could not be looked up. Zernio did not retry it.
  - **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