Ads webhooks
Receive an event when an ads account finishes its first sync, when an ad account stops or resumes syncing, when a Meta Lead Gen form gets a lead, and when an ad object changes status.
Ads events cover the initial backfill after connecting an ads-capable account, ad accounts that stop and resume syncing, real-time leads from Meta Lead Gen forms, and ad object status changes. Subscribe with POST /v1/webhooks/settings and the event names below (first event); delivery, retries and signatures are the same for every event (how webhooks behave).
Events
| Event | Description |
|---|---|
account.ads.initial_sync_completed | The initial 90-day backfill completed for an ads-enabled account. Once per account. |
account.ads.sync_failed | An ad account's ads stopped syncing. Once per episode, until it recovers. |
account.ads.sync_recovered | An ad account reported by account.ads.sync_failed is syncing again. |
lead.received | A new lead was submitted against a Meta Lead Gen form. |
ad.status_changed | An ad, ad set or campaign changed status on the ad platform. Meta only. |
How it behaves
The initial sync reports success or failure once
Zernio runs the initial sync after an ads-capable account is connected: ad-account discovery plus a 90-day historical ad backfill. sync reports whether the backfill succeeded fully or partially and how many ads were synced versus failed. When scoping was applied at connect time (scoping sync to specific ad accounts), account.platformAdAccountId echoes the chosen ad account when the scope is exactly one, and account.platformAdAccountIds lists every act_* synced.
On failure (sync.status is "failure") the payload adds fields so you can branch without parsing prose: sync.error (the raw platform error, truncated to about 2 KB), sync.errorCode and sync.errorSubcode (platform-native codes when parseable, for example Meta 190 or 10), and sync.errorCategory, a stable enum of token_invalid, permission_denied, no_ad_accounts, rate_limited, discovery_failed or unknown. New values may be added; existing ones are stable.
A failed sync is not final. Running the ads connect flow again (Connect ads) re-queues the 90-day backfill and the event fires again with the new outcome; Zernio skips the re-queue only when a backfill has already completed with at least one ad, or when one started less than 20 minutes ago. That is the fix for token_invalid, permission_denied and rate_limited. no_ad_accounts means discovery found no ad account on the grant, so reconnecting changes nothing until the user has one.
A stalled ad account is reported once, and again when it recovers
After the initial sync, Zernio keeps each ad account's ads and metrics fresh on a schedule. An hourly check looks at every ad account that has live ads on a connected account, on any ads platform, and treats it as failing when no sync succeeded for 24 hours, or when every live ad in it has reached the retry cap. account.ads.sync_failed then fires once for that ad account, and nothing more until account.ads.sync_recovered fires once when a sync succeeds again. Metrics for the ad account are stale in between, so this is the signal to flag the numbers in your UI rather than to poll.
adAccount.platformAdAccountId names the ad account (act_* on Meta) and account.accountId the connection it belongs to, so one connection with several ad accounts can report each separately. sync.errorCategory is the field to branch on:
ad_account_not_listed: the platform no longer returns the ad account to this connection. Access was removed, or the platform changed something on its side; check the ad account's permissions for the connected user, then reconnect.sync_error: the platform returned an error on the sync.sync.errorcarries the detail, for display and debugging.stale: no sync succeeded and no error was recorded. Zernio's own retries usually clear it.
New values may be added. sync.lastSuccessfulSyncAt is the last good sync and sync.failureCount the consecutive failed attempts. On account.ads.sync_recovered, sync.failingSince is the last successful sync before the failure started.
A disconnected connection gets account.disconnected instead: its ad accounts are not reported here, and no recovery event follows for them. An ad account that never synced successfully, or whose last good sync is more than 3 days old, is not announced either.
Leads arrive in real time from Meta's Page webhook
Zernio ingests leads through the Page leadgen webhook and forwards each one as lead.received. lead.fields is the flattened question-key to answer map; for multiple-choice questions the value is the option key, for example k1, not the display label. lead.formId, lead.adId and lead.campaignId give provenance, and lead.adId is null for organic or test leads. Deduplicate on lead.leadgenId (Meta's lead id) or the event id. account.profileId names the Zernio profile of the Facebook account that received the lead, so a multi-tenant integration routes leads the same way as every other event; the key is always present and is null only when the lead has no profile on record.
Leads need ads enabled on the team, and the gate is a permission rather than a check at delivery time: Zernio asks for leads_retrieval on the Facebook consent screen only when the team has ads enabled, and Meta refuses the Page's leadgen subscription without it, so no lead ever arrives. A Facebook account connected before ads was enabled needs a reconnect to pick the permission up.
A Page can also lose its leadgen subscription while every other event keeps arriving, so nothing looks wrong. Read the Page's webhook subscription to check: leadgen: false (with a warning) means leads are not reaching Zernio, and Re-subscribe a Page to webhooks re-sends the full field set and returns what Meta actually granted.
Status changes come from two Meta fields
Zernio sources ad.status_changed from two Meta ad_account webhook fields. in_process_ad_objects means the object finished processing and left IN_PROCESS; status.raw carries Meta's status_name (ACTIVE, PAUSED, PENDING_REVIEW, ARCHIVED, DELETED, DISAPPROVED). with_issues_ad_objects means the object entered WITH_ISSUES; status.raw is WITH_ISSUES and error is filled from Meta's error_code, error_summary and error_message.
adObject.level is CAMPAIGN, AD_SET or AD; creative-level events are not forwarded. Branch on status.raw, and use error.code as the stable discriminator: error.summary and error.message are localized to the ad-account owner's Meta locale. error is present on most WITH_ISSUES events, can be absent because Meta does not always include diagnostics, and never appears on any other status, so null-check it before reading error.code.
Matching is keyed on adObject.platformAdAccountId. When several connected metaads accounts point at the same Meta ad account, each receives its own delivery.
Subscribing to ad.status_changed needs ads access on the team: without it POST /v1/webhooks/settings answers 403 with code ads_addon_required. Usage-based billing includes ads on every account (pricing).
account.ads.initial_sync_completed
The initial sync completed for an ads-enabled account: ad-account discovery plus the 90-day backfill. sync carries the outcome and, on failure, the error fields described above.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Webhook payload for account.ads.initial_sync_completed events.
Fired once per ads-enabled account when the initial discovery + 90-day
ad backfill finishes (whether it succeeded fully, partially, or failed).
Response Body
Example Requests
Webhook payload for account.ads.initial_sync_completed events.
Fired once per ads-enabled account when the initial discovery + 90-day
ad backfill finishes (whether it succeeded fully, partially, or failed).
/account.ads.initial_sync_completedaccount.ads.sync_failed
An ad account's ads stopped syncing: no successful sync for 24 hours, or every live ad at the retry cap. Fires once until the ad account recovers.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Webhook payload for account.ads.sync_failed events. Fired once per ad account
when its ads stop syncing: no successful sync for 24 hours, or every live ad in it
at the retry cap. It does not fire again for the same ad account until
account.ads.sync_recovered. Metrics for the ad account are stale meanwhile.
Response Body
Example Requests
Webhook payload for account.ads.sync_failed events. Fired once per ad account
when its ads stop syncing: no successful sync for 24 hours, or every live ad in it
at the retry cap. It does not fire again for the same ad account until
account.ads.sync_recovered. Metrics for the ad account are stale meanwhile.
/account.ads.sync_failedaccount.ads.sync_recovered
An ad account previously reported by account.ads.sync_failed synced successfully again.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Webhook payload for account.ads.sync_recovered events. Fired once when an ad account
previously reported by account.ads.sync_failed syncs successfully again.
Response Body
Example Requests
Webhook payload for account.ads.sync_recovered events. Fired once when an ad account
previously reported by account.ads.sync_failed syncs successfully again.
/account.ads.sync_recoveredlead.received
A new lead was submitted against a Meta Lead Gen (Instant) form. lead.fields holds the answers keyed by question.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Webhook payload for lead.received events (Meta Lead Gen / Instant Forms).
Response Body
Example Requests
Webhook payload for lead.received events (Meta Lead Gen / Instant Forms).
/lead.receivedad.status_changed
A campaign, ad set or ad on a connected Meta ad account (metaads) changed status. status.raw is Meta's status name and error is present when the object entered WITH_ISSUES.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Webhook payload for the ad.status_changed event. Currently emitted
only for Meta (metaads).
Sourced from two Meta ad_account webhook fields:
in_process_ad_objects- the ad object finished processing and exitedIN_PROCESS.status.rawcarries Meta'sstatus_name.with_issues_ad_objects- the ad object enteredWITH_ISSUES.status.rawisWITH_ISSUESand theerrorblock is populated from Meta'serror_code/error_summary/error_message.
Review outcomes (an ad leaving PENDING_REVIEW for ACTIVE,
DISAPPROVED and so on) are also emitted from Zernio's own ad sync,
so they arrive even when Meta skips the webhook. status.raw is Meta's
effective_status. An ad-level outcome is delivered once per status:
whichever source sees it first sends it.
Response Body
Example Requests
/ad.status_changedRelated
- Webhooks: create an endpoint, retries, signatures.
- Meta Ads: campaigns, boosting and lead forms.
- Connecting accounts: scope the sync to specific ad accounts.
- List leads: the same leads on demand.