Changelog
Every change to the Zernio API, newest first.
Significant changes are announced here, on the Telegram channel and on X. Every entry is tagged with the platforms and areas it touches: filter below by platform and type, and share the URL (for example /changelog?platform=whatsapp&type=breaking_change). The same filters work on the RSS feed (/changelog/feed.xml?platform=whatsapp) and on /changelog.json. To automate on changes, subscribe a webhook to api.changelog.published: each entry arrives as it is published, with its tags and the diff of the OpenAPI spec behind it as data. List API changelog entries returns the same entries, with a platform filter, for back-filling.
Nothing below breaks a working integration. Every endpoint is versioned in the URL path, currently /v1, and a breaking change ships only as a new path version: /v1 keeps working. New endpoints, new response fields and new error codes arrive inside /v1 at any time, which is why error handling asks you to branch on code. An operation on its way out is marked deprecated: true in the OpenAPI spec and announced here before it is removed.
844 entries
These endpoints now return more explicit auth/connection failure reasons so clients can distinguish invalid API credentials from revoked/expired social tokens and prompt a reconnect.
Pinterest boards:
GET /v1/accounts/{accountId}/pinterest-boards•
401 - missing/invalid API key (missing_credentials / invalid_credentials) or Pinterest token revoked/expired (reconnect_required)•
409 - no stored access token (ads_connection_required)•
502 - Pinterest rejected/failed the requestYouTube playlists:
GET /v1/accounts/{accountId}/youtube-playlists•
401 - missing/invalid API key (missing_credentials / invalid_credentials) or YouTube token revoked/expired (reconnect_required)502 when the connected WhatsApp account is missing a Business Account ID, so the approved template definition can’t be looked up (no message is sent).This affects template lookup failures on:
•
POST /v1/inbox/conversations•
POST /v1/inbox/conversations/{conversationId}/messagesIf you see
502 with a WhatsApp template send, check that the account has a valid WABA/Business Account ID; a missing approved template language variant is still a 400.GET /v1/reddit/comments/{postId} to read a Reddit post’s comments (fetched from Reddit on each call).Returns a flattened thread in reply order; rebuild the tree using
parentId (post replies use t3_…, comment replies use t1_…). Use more + commentId to expand truncated threads.Key params:
•
postId (path) – with or without t3_•
accountId (query, required)•
sort (query) – new | top | best | controversial | old | qa•
limit (query, max 100)•
commentId (query) – fetch a specific comment + its replies (including ids from more)Response shape:
•
post (nullable)•
items (array of RedditComment)•
more (array of omitted comment ids)Errors:
403/404 may return platform_api_error when Reddit refuses or no longer serves the thread.warnings on success.This lets you detect cases where Meta created the ad set differently than requested (e.g. the ad set was created without the requested
whatsappPhoneNumber in promoted_object, while the ads still carry it on their WhatsApp button).Check
warnings: string[] in:•
CtwaSingleResponse•
CtwaMultiResponseringTimeoutSeconds on POST /v1/voice/calls and POST /v1/voice/calls/web/dial.Use it to control how long the callee rings before the call ends as
no_answer (carrier may end sooner).New parameter:
ringTimeoutSeconds (integer, min 5, max 120, default 30).These endpoints may now also return
400 (BadRequest) for invalid requests.GET /v1/ads/{adId}/review now works on Google by reading ad_group_ad.policy_summary live, returning approval/review details and policy topics.New/Google-only fields in
review:•
approvalStatus = APPROVED | APPROVED_LIMITED | AREA_OF_INTEREST_ONLY | DISAPPROVED | UNKNOWN•
reviewStatus (Google) = REVIEW_IN_PROGRESS | REVIEWED | UNDER_APPEAL | ELIGIBLE_MAY_SERVE•
policyTopics[] with type = PROHIBITED | LIMITED | FULLY_LIMITED | DESCRIPTIVE | BROADENING | AREA_OF_INTEREST_ONLY plus Google evidences/constraints verbatimNote:
forbidden* arrays remain TikTok-only (empty on Google). 501 is now “TikTok and Google”.GET /v1/ads/activity now also supports Google by reading the customer change_event history (last 30 days) and mapping it onto the Meta row shape.Key behavior/fields:
•
adAccountId can be Meta act_<n> or Google customer id (digits)• Google keeps 30 days:
since defaults to 29 days ago; older since returns 400• Google rows include
changed_fields[] and extra_data = JSON string { old, new }•
501 is now “Meta and Google”POST /v1/ads/conversions/actions now lets you set more Google Ads conversion action settings at creation.You can optionally configure category, counting, value settings, lookback windows, and whether the action is primary for its goal.
New optional fields:
•
category: DEFAULT, PAGE_VIEW, PURCHASE, SIGNUP, DOWNLOAD, ADD_TO_CART, BEGIN_CHECKOUT, SUBSCRIBE_PAID, PHONE_CALL_LEAD, IMPORTED_LEAD, SUBMIT_LEAD_FORM, BOOK_APPOINTMENT, REQUEST_QUOTE, GET_DIRECTIONS, OUTBOUND_CLICK, CONTACT, ENGAGEMENT, STORE_VISIT, STORE_SALE, QUALIFIED_LEAD, CONVERTED_LEAD•
countingType: ONE_PER_CLICK | MANY_PER_CLICK•
defaultCurrency (ISO-4217)•
clickThroughLookbackWindowDays (1–90)•
viewThroughLookbackWindowDays (1–30)•
primaryForGoalPATCH /v1/ads/conversions/actions/{actionId} now updates multiple conversion action settings (not just primaryForGoal).Send
accountId plus at least one field to update (minProperties: 2). Supported fields include name, primaryForGoal, category, countingType, defaultValue, defaultCurrency, alwaysUseDefaultValue, clickThroughLookbackWindowDays, viewThroughLookbackWindowDays, and status.status: ENABLED | REMOVED (note: REMOVED must be sent alone; ENABLED restores a removed action).You can list and create shared budgets, then move campaigns onto them via the campaign update endpoint.
New endpoints:
•
GET /v1/ads/shared-budgets with accountId (required) and optional adAccountId•
POST /v1/ads/shared-budgets body: accountId, name, amount, optional adAccountId, type (daily | lifetime; only daily is accepted, lifetime returns 422)Campaign updates (Google):
•
PUT /v1/ads/campaigns/{campaignId} now accepts sharedBudgetId ("123" or null)• Moving onto a shared budget requires
allowSharedBudgetUpdate: true (otherwise 409)•
sharedBudgetId cannot be sent together with budget•
sharedBudgetId: null moves the campaign back to its own budget and requires budget (Google: daily) in the same requestOnly available for Google Ads; other platforms return 501.
sender.status=awaiting_payment from POST /v1/imessage/senders/order.If the first-month card charge confirmation takes longer than the request, the order is accepted (202) and stays in
awaiting_payment; once payment confirms, the number is ordered automatically (usually under 5 minutes). Poll GET /v1/imessage/senders/{senderId} until it moves to ordering/activating/active.Updated
ImessageSenderLifecycle.status enum:ordering, awaiting_payment, activating, active, suspended, canceled, failedNote:
409 no longer includes the prior “retry within the hour with purchaseIntentId” case; use purchaseIntentId for idempotent retries as before./v1/ads/ad-sets now returns platform-native targeting in targeting across all supported ads platforms (not just Meta/LinkedIn).This makes it possible to read back the actual synced targeting shape per platform (useful for audits, diffing, and “read-modify-write” flows) from the ad set list response.
Key shapes you’ll see in
targeting:• TikTok: adgroup/get targeting fields (e.g.
placements, location_ids, age_groups, audience_ids, excluded_audience_ids)• Google:
campaignCriteria + adGroupCriteria (criterion types like LOCATION, LANGUAGE, DEVICE, AGE_RANGE, GENDER, USER_LIST, INCOME_RANGE, PARENTAL_STATUS)• Pinterest:
targeting_spec verbatim• X:
targeting_criteria verbatim• OpenAI: campaign
targeting + ad group context_hintstargeting can still be null for ad sets not re-synced yet (or if targeting reads have consistently failed).You can create/update portfolio bid strategies with
type=TARGET_IMPRESSION_SHARE, and set campaign-level Target Impression Share or Manual CPC directly when creating/updating campaigns.New/updated fields:
•
POST /v1/ads/bid-strategies / PATCH /v1/ads/bid-strategies/{strategyId}: type now includes TARGET_IMPRESSION_SHARE, plus targetImpressionShare•
POST /v1/ads/campaigns: targetImpressionShare, manualCpc, networkSettings•
PUT /v1/ads/campaigns/{campaignId}: targetImpressionShare, manualCpc, networkSettings, trackingUrlTemplate, finalUrlSuffix•
PATCH /v1/ads/accounts (Google only): trackingUrlTemplate, finalUrlSuffix•
POST /v1/ads/ad-sets (Google only): maxCpcTarget Impression Share config (
targetImpressionShare):•
location: ANYWHERE_ON_PAGE | TOP_OF_PAGE | ABSOLUTE_TOP_OF_PAGE•
percent (0-100)•
maxCpc (>0)Notes:
•
targetImpressionShare is exclusive with bidStrategy, portfolioBidStrategyId and manualCpc on Google campaigns.• Manual CPC uses
manualCpc: { maxCpc } and can be paired with POST /v1/ads/ad-sets maxCpc to avoid Google’s default 0.01 ad group bid.You can now fetch and manage the campaign’s location exclusions separately from included locations.
GET
/v1/ads/campaigns/{campaignId}/targeting adds:•
excludedLocations (negative location criteria)•
excludedLocationsEditable (whether PUT accepts excludedLocations; false on Demand Gen)PUT
/v1/ads/campaigns/{campaignId}/targeting accepts:•
targeting.excludedLocations (same shapes as locations: country-code array or { countries, regions, cities, zips, metros })Notes:
•
excludedLocations replaces the excluded set; [] (or {}) removes all exclusions• A place can’t be both included and excluded; conflicting requests return
400• Demand Gen:
excludedLocations is not supported yet and returns 400GET /v1/ads/accounts/live now supports TikTok in addition to Meta, letting you read campaigns + ad sets (TikTok ad groups) live from the platform before writes.Use the same request shape with
adAccountId as either Meta act_<n> or a TikTok advertiser id (digits). Response platform is now facebook or tiktok.TikTok-specific notes:
•
status accepts a single value only: ACTIVE | PAUSED | DELETED (multiple values or IN_PROCESS/WITH_ISSUES/CAMPAIGN_PAUSED/ARCHIVED return 400)• Pagination: when using
after on TikTok, you must send the same limit that produced the cursor (changing limit returns 400)• New field:
budgetMode (TikTok budget_mode) on campaigns and ad sets• On TikTok,
budgetRemaining and spendCap are always null, and campaign bidStrategy is nullPlatformAnalytics now distinguishes Facebook posts you don’t own when syncing analytics.You can detect collab/visitor posts where analytics can’t be read with the connected Page token.
New/updated fields:
•
errorCode adds not_post_owner (enum: post_not_found, not_post_owner, permission_missing, null)•
isOwner (Facebook only): true when the connected Page authored the post, false when another author is reported, null when unknown/other platformsYou can attach public image URLs as assets, and list attached images (returned as Google-hosted copies with dimensions).
Attach images with:
•
POST /v1/ads/campaigns/{campaignId}/assets → images (array of image URLs)•
POST /v1/ads/ad-sets/{adSetId}/assets → images (array of image URLs)List images via:
•
GET /v1/ads/campaigns/{campaignId}/assets → images•
GET /v1/ads/ad-sets/{adSetId}/assets → imagesNotes:
•
images: 1.91:1 (min 600x314) or 1:1 (min 300x300), up to 5 MB each• Response includes
imageUrl, width, height and imageAssetResourceNames• Google may show images only for eligible accounts
truncated in GET /v1/connect/facebook/select-page.This indicates when the returned
pages list is incomplete because Meta hit a time budget or the 10,000 Page cap, so you can handle large accounts safely.New field:
•
truncated (boolean) — true means pages is incomplete. Don’t prompt users to reconnect with fewer Pages selected (Meta replaces the Page grant on re-authorization, and unticked Pages lose access)./v1/ads/{adId}/media now returns media for boosted Instagram posts more explicitly.If an ad creative has no image of its own, the endpoint returns the original post’s media (single image/video, or carousel children). If the connection can’t read the Instagram post, it falls back to a 1080px rendering of the creative.
Response fields to use:
•
media[].type (image | video)•
media[].url•
media[].thumbnailUrl (videos)•
media[].index (carousel children)PlatformAnalytics now includes errorCode alongside errorMessage.This provides a stable, machine-readable reason you can use to branch on common analytics/publishing error cases (instead of parsing
errorMessage).New field:
errorCode ∈ post_not_found, permission_missing, null (more values may be added).This makes these DELETE calls easier to use from clients/tools that don’t reliably send bodies on DELETE.
Updated endpoints:
•
DELETE /v1/ads/campaigns/{campaignId}/assets now uses accountId, adAccountId/customerId, assetResourceNames[], campaignAssetResourceNames[]•
DELETE /v1/ads/ad-sets/{adSetId}/assets now uses accountId, adAccountId/customerId, assetResourceNames[], adGroupAssetResourceNames[]•
DELETE /v1/ads/labels/{labelId}/assignments now uses accountId, adAccountId/customerId, plus one or more of campaignIds[], adSetIds[], adIds[], keywordIds[]•
DELETE /v1/ads/accounts/callouts, /sitelinks, /structured-snippets now use accountId, adAccountId/customerId, assetIdAlso:
•
POST /v1/ads/campaigns/bulk-status now accepts platform aliases in platform: googleads, tiktokads, linkedinads, pinterestads, xads, openaiads (alongside google, tiktok, linkedin, pinterest, twitter, openai; metaads is not accepted)•
PATCH /v1/ads/{adId}/tracking-tags (Google) is documented as writing at the campaign level (applies to all ads in the campaign)•
GET /v1/ads/keywords and KeywordEntry.matchType are now documented as case-insensitive (stored lowercase)tiktok, threads, linkedin, youtube) all DM-leg fields are omitted (not just empty).If your client assumes these fields exist, update parsing to treat them as optional based on
platform.Key detail:
•
dmMessage (and related DM fields) are omitted on platform = tiktok/threads/linkedin/youtubeAlso,
PATCH /v1/comment-automations/{automationId} can now re-bind an automation to a different post/story by sending any of:•
platformPostId, postId, postTitle (they move as a unit; omitted ones are cleared)Rebinding can return
409 if another active automation already owns the target post.Workflows
- New triggers:
referral (ig.me / m.me ref links and ad clicks), story_mention, comment, reaction, whatsapp_event, plus cooldownHours.- Variables:
referral.*, postback.*, quickReply.*, contact.tags, contact.fields, contact.isFollower; numeric, date and tag operators in conditions.- Instagram and Messenger
send_message takes buttons, quickReplies, messageTag and messageType: cards; text with buttons goes out as a button template (640 characters).- A button tap targeting a workflow that is waiting for a reply answers that wait.
-
workflowId on POST /v1/ads/messaging: an ad click starts that workflow.Comment automations
-
repeatPolicy (every comment, with cooldown), dedupeSameTextHours, publicReplyPolicy: always, actions.likeComment / hideComment, quickReplies, dmMedia, {{first_name}} variables, isActive on create.- Triggers
live_comment and story_mention.- Public-reply automations on TikTok, Threads, LinkedIn and YouTube.
- A reconcile job replays Instagram and Facebook comments whose webhook Meta dropped.
Meta handover
-
thread-control pass / take / request / release on Facebook and Instagram, standby messages carry metadata.standby, conversation.control_changed fires; a send on a thread another app owns answers 409 not_thread_owner.- Instagram persistent menu via
messenger-menu.Broadcasts:
segmentFilters.customFields applied, SMS delivery counters, MMS attachments, messageTag on Instagram and Facebook.Behaviour change: an inbox send on Instagram or Facebook with
buttons now goes out as a button template; text over 640 characters with buttons returns 400 (it used to become a card with an 80-character title).[ManyChat-style patterns guide](https://docs.zernio.com/guides/manychat-patterns) · [Workflows](https://docs.zernio.com/workflows) · [Automation events](https://docs.zernio.com/webhooks/automations)
GET /v1/connect/{platform}.You can prevent connecting the “wrong” social account by requiring the authorized login to match an expected id and/or handle; if it doesn’t match, the flow ends with
error=account_mismatch and nothing is written.New query params:
•
expectedPlatformUserId (max 100)•
expectedUsername (max 100, case-insensitive, leading @ ignored)Notes:
• If both are provided, both must match.
• In headless mode, these markers are carried via the returned
redirect_url (pass it through unchanged).Instagram Facebook-Login selection now returns a dedicated mismatch error:
•
POST /v1/connect/instagram/select-account can return 409 with code=account_mismatch.Accounts now expose a stable platform id for matching:
•
SocialAccount.platformUserIdcreative.carouselCards.This lets you render carousel previews (and inspect card-level links/CTAs) directly from Zernio responses, without reconstructing cards from raw Meta creative payloads.
New field:
•
creative.carouselCards[] with imageUrl, imageHash, headline, description, linkUrl, callToAction, videoIdOnly present for Meta carousel ads; absent for non-carousel ads/other platforms. Older synced carousels will populate
carouselCards on their next sync.You can connect a Whop account using an Account API key, then list/install the Whop Pixel on Shopify or WordPress. The Whop account id (
biz_...) is used as the pixel/tag id.Connect Whop:
•
POST /v1/connect/whop-ads/credentials• Body:
apiKey, profileId (optional: state, redirect_url)• Returns:
accountId (Zernio ads account), whopAccountId (biz_...)Tracking tags now accept
platform=whopads on:•
GET /v1/accounts/{accountId}/tracking-tags•
GET /v1/accounts/{accountId}/tracking-tags/{tagId}•
POST /v1/accounts/{accountId}/tracking-tags/{tagId}/install•
DELETE /v1/accounts/{accountId}/tracking-tags/{tagId}/install•
GET /v1/accounts/{accountId}/tracking-tags/{tagId}/statsAccounts health filtering also adds
platform=whopads on GET /v1/accounts/health.GET /v1/inbox/conversations using folder=requests, then accept by replying via POST /v1/inbox/conversations/{conversationId}/accept (body: accountId, message; supports Idempotency-Key).You can now manage Facebook Messenger greeting and ice breakers:
•
GET|PUT|DELETE /v1/accounts/{accountId}/messenger-greeting•
GET|PUT|DELETE /v1/accounts/{accountId}/messenger-ice-breakersIce breaker payloads can start workflows with
zernio:workflow:<workflowId>.WhatsApp now supports conversational automation (prompts + slash commands):
•
GET /v1/whatsapp/conversational-automation?accountId=...•
POST /v1/whatsapp/conversational-automation (body: accountId, optional enable_welcome_message, prompts, commands)•
DELETE /v1/whatsapp/conversational-automation?accountId=...Workflows can now be started via API with
POST /v1/workflows/{workflowId}/trigger. Provide exactly one target: conversationId | contactId | to (WhatsApp only), plus optional variables.Webhook subscriptions can now include new events:
contact.tag_added, contact.tag_removed, contact.field_changed, sequence.enrolled, sequence.exited, workflow.run.started, workflow.run.completed, workflow.run.failed.Note: sending to Meta threads you don’t own can now return
409 with code not_thread_owner; take control first via POST /v1/inbox/conversations/{conversationId}/thread-control.GET /v1/analytics/instagram/online-followers.You can now fetch how many Instagram followers were online per hour for each day (up to the last 30 days of Meta-provided data), useful for picking optimal posting times.
Use
accountId (query) to select the Instagram SocialAccount.Returns
platform=instagram, metric=online_followers, and days[] with endTime + hours keyed "0"…"23".Notes: data can be delayed up to 48h; accounts with <100 followers may return an empty
days; requires the Analytics add-on (may return 402 with code=analytics_addon_required).MediaItem.subtitles.Provide up to 20 tracks (SRT/WebVTT). Zernio fetches and validates each file when the post is created/updated, converts as needed per platform, and skips unsupported tracks with response warnings.
Use:
•
subtitles (array, max 20)•
subtitles[].url (public direct http(s) URL; redirects refused)•
subtitles[].language (BCP-47, e.g. en, pt-BR)Uploads via
POST /v1/media/presign now accept contentType values: application/x-subrip, text/vtt.Validation: unreachable/redirecting files, >1 MB, invalid SRT/VTT, or platform-limit violations are rejected with
400 (INVALID_SUBTITLES).clip.id as tiktokSettings.musicSoundInfo.musicSoundId (fall back to id only if clip is missing).Both IDs can publish, but using the full-track
id has been observed to show viewers “This song is not available in your country” on the sound page (e.g., Germany), while clip.id opens a working sound page for the same track.Use:
•
GET /v1/accounts/{accountId}/tiktok/commercial-music → tracks[].clip.id• Post creation →
tiktokSettings.musicSoundInfo.musicSoundIdadStatus: PAUSED now create the LinkedIn creative as DRAFT (LinkedIn does not allow PAUSED until review is APPROVED).A
DRAFT creative is not submitted for review and cannot serve; it will read back as paused. To start review and delivery, resume the ad via PUT /v1/ads/{adId}/status with active.Applies to:
•
POST /v1/ads/boost with adStatus•
POST /v1/ads/create (attach shape via adSetId) with adStatusKey field:
adStatus (ACTIVE | PAUSED)/v1/ads/{adId} can now also clean up empty parent objects on Meta.When cancelling an ad, Zernio may delete the now-empty
ad set and then the campaign too, but only if they were created by Zernio (via POST /v1/ads/create or POST /v1/ads/boost) and Meta reports no other ads/ad sets (archived ads count). Parents created outside Zernio (or from imported ads) are always kept.If you want to delete a whole campaign explicitly, use
DELETE /v1/ads/campaigns/{campaignId}.Also,
POST /v1/ads/boost now supports per-level publish control:•
campaignStatus: ACTIVE | PAUSED•
adSetStatus: ACTIVE | PAUSED•
adStatus: ACTIVE | PAUSEDThese override
status for their respective level (and are rejected with adSetId/existingCampaignId where that level isn’t created).POST /v1/ads/messaging via carouselCards.You can create a 2–10 card image carousel where every card opens the same conversation (WhatsApp/Messenger/Instagram Direct), instead of using
imageUrl or video.Use
carouselCards (2–10 items) with:•
imageUrl (required)•
headline (optional)•
description (optional)Notes:
•
carouselCards replaces imageUrl/video (sending either is a 400)• Requires
body• Not compatible with
placementAssets or existing-post fields (platformPostId/objectStoryId/existingPostId)•
validateOnly: true now supports carousel validation/v1/sms/messages can now return 403 when the API key is profile-scoped and the from sender belongs to a profile outside the key’s scope (including alphanumeric sender IDs).This helps surface sender-scope issues explicitly instead of failing in other ways.
Handle the new response:
•
403 - profile-scoped key cannot use the provided fromcomment.received webhook payload now includes Instagram ad context comment.ad.originalMediaId.This lets you correlate comments on paid Instagram content to the original media Meta reports for the ad (when Meta includes it).
New field:
•
comment.ad.originalMediaId (Instagram only, optional)Event:
comment.receivedWhen you hit a Zernio-side burst limit (before the call reaches Google), the error
details can include budgetScope so you can throttle correctly per scope.Look for
details.budgetScope:•
account - 15 requests/min per connected Google Ads account•
user - 120 requests/min per Zernio user across all their Google Ads accountsApplies to
POST /v1/ads/keywords/ideas and POST /v1/ads/keywords/historical-metrics (429 responses).GET /v1/analytics/post-timeline now returns follows as nullable (integer | null).This reflects platform limitations: for some Meta media types the follows metric isn’t available, so it will be
null instead of 0.Update your parsing/aggregation logic to handle:
•
follows: null (not reported)•
follows: 0 (reported, but zero)Key field:
timeline[].followsGET /v1/ads/accounts/live reads a Meta ad account’s campaigns and ad sets live from Meta (no sync delay) in one request, and stores nothing.Use it when you need current
daily_budget/lifetime_budget, bid settings, targeting, etc. before making writes (e.g. spend gates).Required params:
•
accountId (Zernio SocialAccount id)•
adAccountId (Meta act_<n>)Optional params:
•
status (comma-separated Meta effective_status; defaults to all except DELETED, ARCHIVED; ad sets also support CAMPAIGN_PAUSED)•
limit (1–500, default 200)•
level = campaign | adSet (required with after)•
after (cursor from paging.campaigns.after / paging.adSets.after)Notes:
• Budgets/bids are returned in whole units of
currency (Meta minor units converted).• Other platforms return
501 (Meta only today).Use
POST /v1/ads/videos with async: true to return immediately (202) after Meta accepts the bytes, then wait for processing to finish before using video.id on create endpoints.New endpoint:
GET /v1/ads/videos/{videoId}Required:
videoId (path), accountId (query), adAccountId (query)Returns
video.status: processing | ready | error plus platformStatus, processingProgress (0-100), error, thumbnailUrl.Upload responses:
•
201 (default): video.status: ready•
202 (async: true): video.status: processingWebhook: subscribe to
ad.video.processed (fires for async: true uploads) with video.status: ready | error.GET /v1/ads/campaigns by a specific platform campaign ID using campaignId.This lets you fetch exactly one campaign (or an empty list if it isn’t visible to the caller), matching the same filter behavior as
/v1/ads and /v1/ads/tree.Use:
campaignId=<platformCampaignId>Ad set duplication now supports TikTok via
POST /v1/ads/ad-sets/{adSetId}/duplicate.Set
platform to tiktok (in addition to facebook, instagram). Note: TikTok ignores startTime, endTime, and renameStrategy; renamePrefix/renameSuffix still apply, and statusOption must be PAUSED (or omitted)./v1/ads/create now supports per-level status controls via campaignStatus, adSetStatus, and adStatus (each: ACTIVE | PAUSED).This lets you choose exactly which level is created paused (campaign vs ad set vs ad), instead of relying only on the top-level
status behavior.Use:
•
campaignStatus to pause/activate only the new campaign (ignored when using existingCampaignId or adSetId)•
adSetStatus to pause/activate only the new ad set (400 with adSetId)•
adStatus to pause/activate only the new ad (works with adSetId; X returns 400)Precedence: level statuses win for their level;
status: PAUSED only adds a “hold” when none of the level statuses are PAUSED. To create everything paused, set all three to PAUSED./v1/accounts/{accountId}/facebook-page now returns the Instagram professional account linked to each Facebook Page via pages[].instagramAccount.This lets you discover the IG account tied to a Page (and use its id when creating ads). The endpoint also now works for both
facebook and metaads accounts.Key fields:
•
pages[].instagramAccount (object|null) with id and username•
selectedPageId is now string|null (null for classic metaads connections)• If your cached list predates this field, call with
refresh=true to populate itPUT
/v1/accounts/{accountId}/facebook-page now clarifies behavior for metaads: classic metaads connections return 400 (no default Page); pass pageId per ad on POST /v1/ads/create instead. It can also return 409 with reconnect_required when the Page has no stored access token.GET /v1/ads/insights documents consistent 429 behavior.For Meta throttles (codes
4, 17, 32, 613, 80000-80014), Zernio returns 429 rate_limited with a Retry-After header (even when Meta responds with HTTP 400).Handle
429 by reading:•
Retry-After (seconds)• JSON error body:
ErrorResponseApplies to:
GET /v1/ads/insights, POST /v1/ads/insights/reports, GET /v1/ads/insights/reports/{reportRunId}, POST /v1/ads/preview, GET /v1/ads/{adId}/preview.GET /v1/accounts/{accountId}/instagram/business-discovery.You can now look up any public Instagram Business or Creator account by
username and retrieve its public profile plus most recent media (useful for competitor/market research).Call with:
•
accountId (path) - a connected Instagram account via Facebook Login•
username (query) - handle with/without @•
limit (query, default 12, max 25)Response includes
profile and media (most recent first). Media fields include mediaType (IMAGE/VIDEO/CAROUSEL_ALBUM) and mediaProductType (FEED/REELS); likeCount is null when hidden.Note: requires Facebook Login; classic Instagram Login will return 400
instagram_business_discovery_requires_facebook_login.GET /v1/analytics/facebook/demographics returns Facebook Page follower demographics (country/city) from Meta’s latest daily snapshot.Use
accountId (required) and optional breakdown (country, city; defaults to both). Response includes snapshotDate (may be null) and demographics.country/demographics.city arrays.Requires Analytics access; missing access returns
402 (analytics_addon_required).Also updated:
DELETE /v1/ads/audiences/{audienceId} now documents platform-specific removal behavior and adds 422 when a platform can’t remove an audience via API (e.g., Google lookalike lists) or refuses removal; on refusal, the Zernio record is kept so retries are safe.tiktok_engagement and pinterest_engagement.You can create and list these via
POST /v1/ads/audiences and GET /v1/ads/audiences using type = tiktok_engagement or pinterest_engagement.Key request fields:
• TikTok engagement:
type=tiktok_engagement, retentionDays, source (ads | organic_video | live_video | business_account), event, plus sourceIds/identityId/identityType as required• Pinterest engagement:
type=pinterest_engagement, optional engagerType, engagementType (click | save | closeup | comment | like), and at least one of engagementDomains/campaignIds/adIds/pinIdsAlso updated:
website and lookalike audience creation now supports additional platforms with platform-specific validation (e.g. Google website rejects pixelId; TikTok/Google lookalike uses size (narrow | balanced | broad) instead of ratio)./v1/analytics/post-timeline now returns Facebook Reels retention metrics via two new fields: replays and retentionCurve.This lets you track how many plays were replays and how viewer retention changes over time (per day, per platform row). These fields are populated for Facebook Reels and return
0/{} elsewhere.New fields:
•
replays - Facebook Reels replay count•
retentionCurve - map of "second" → fraction (0..1) still watching at that secondigReelsAvgWatchTime and igReelsVideoViewTotalTime analytics now cover more than Instagram Reels.These watch-time metrics are now reported for Instagram Reels, Facebook Reels, and TikTok videos (business accounts), instead of Instagram Reels only.
Key fields:
•
PostAnalytics.igReelsAvgWatchTime (ms per play)•
PostAnalytics.igReelsVideoViewTotalTime (ms total, incl. replays)•
AnalyticsDeltaEntry.metrics.igReelsAvgWatchTime•
AnalyticsDeltaEntry.metrics.igReelsVideoViewTotalTimeadSetId) now supports setting a call number via phoneNumber.Use this when adding an ad to a Meta "website and phone call" ad set (destination_type
WEBSITE_AND_PHONE_CALL). If omitted, Zernio reuses the number from existing ads in the ad set; if none exists, the request returns 400.Set
phoneNumber as E.164:•
+4712345678 (pattern ^\+[1-9]\d{6,14}$)Constraints:
• Only allowed with
adSetId (Meta attach shape)• Rejected with
existingCreativeId• Rejected on non-call ad sets and on non-Meta platforms
POST /v1/ads/create now support website cards.You can create an X website card by sending
imageUrl + headline + linkUrl together (the URL is not added to the post text).X rules:
• Website card requires
imageUrl, headline, and linkUrl (sending only some of them returns 400)•
description, video, and callToAction are rejected with 400 on Xtargeting.interests.On Google, interests are applied as ad-group interest criteria (affinity + in-market) for newly created Search/Display ad groups.
Use
targeting.interests = [{ id, name? }].Constraints on Google:
• Not supported on
Performance Max (400)• Not supported on
Demand Gen (use Demand Gen audience userInterests)• Not allowed when attaching to an existing ad group via
adSetId (400)•
interests are create-only on GooglesmartTargeting.This lets TikTok expand delivery beyond your selected audiences/interests using TikTok’s smart targeting flags.
Available on:
•
PUT /v1/ads/ad-sets/{adSetId} (TikTok only; 400 on other platforms)•
POST /v1/ads/boost (TikTok only; not allowed with smartPlus: true or when attaching via adSetId)•
POST /v1/ads/create (TikTok only; not allowed with smartPlus: true or when attaching via adSetId)Set:
•
smartTargeting.audience (boolean)•
smartTargeting.interestsBehaviors (boolean)Only the flags you send are written; send
false explicitly if you need to verify it’s off (unsent flags may read back as null in native settings)./v1/ads/targeting/search now supports TikTok advertiser scoping via a new optional adAccountId query parameter.If your TikTok connection contains multiple advertisers, you can now choose which advertiser’s targetable markets/catalogs are used for geo/targeting searches (instead of defaulting to the first advertiser).
Use:
•
adAccountId - TikTok advertiser id to search as (optional; invalid/unowned advertiser returns 400)/v1/ads/targeting/search now returns platformId on country results.This lets you keep using
id as the ISO 3166-1 alpha-2 code for targeting.countries, while also having the platform’s native country identifier available for reconciliation with platform reads.Key fields on a
country result:•
id = ISO country code (e.g. GB)•
platformId = platform-native country id (e.g. TikTok location_id/GeoNames id, Google geo target constant id, LinkedIn geo URN)Only present when
type is country (dimension geo)./v1/ads/ad-sets now returns TikTok live configuration details when you pass live=true.For TikTok ad sets read live, you can now inspect the platform-applied settings verbatim (not stored) to verify what TikTok actually applied (budget, schedule, placements, targeting, exclusions, etc.).
New fields (TikTok only, only with
live=true and only on rows read live):•
nativeSettings - TikTok adgroup/get record verbatim (+ advertiser_currency, advertiser_timezone)•
configReadAt - timestamp when nativeSettings was read (null if not read now)Also added to the
AdCampaign schema for TikTok live reads:•
nativeSettings•
configReadAtGET /v1/ads/{adId}/review returns the platform’s review verdict for an ad.This lets you read TikTok’s audit outcome (including partial delivery restrictions and detailed rejection reasons) without re-enabling a paused ad.
Key params/fields:
• Path:
adId (Zernio ad id or platform ad id)• Response:
review.approved, review.reviewStatus (ALL_AVAILABLE | PART_AVAILABLE | UNAVAILABLE)• Restrictions:
review.forbiddenPlacements, review.forbiddenAges, review.forbiddenLocations, review.forbiddenOperatingSystems• Rejections:
review.rejections[] with reasons[], suggestion, and reviewed contentTikTok-only: other platforms return
501. 404 if the ad isn’t found, has no TikTok ad id yet, or TikTok has no review record./v1/ads/ad-sets now returns TikTok’s applied optimization goal and billing event when using live=true.This lets you verify what TikTok actually applied (from
adgroup/get), not just what was requested.With
platform=tiktok and live=true (requires campaignId or adSetId), rows read live may include:•
optimizationGoal (e.g. ENGAGED_VIEW, ENGAGED_VIEW_FIFTEEN, CLICK, CONVERT)•
billingEvent (e.g. CPV, CPC, OCPM)Only present for TikTok and only on rows that were read live (others will be absent/null).
You can connect several GBP locations / Discord channels / Slack channels at once (up to 25). When you send 2+ distinct ids, the response returns
accounts and failed (instead of account), and may return 422 if none could be connected.Google Business Profile:
POST /v1/connect/googlebusiness/select-location• Request now requires only
profileId + pendingDataToken• Send
locationId or locations (not both)•
locations[]: { locationId, accountId? }• New response fields for batch:
accounts, failed; new 422 when none connectDiscord:
POST /v1/connect/discord•
channelId is no longer required (breaking)• Send
channelId or channelIds (not both)• New
redirect_url (only for channelIds) and new 422 when none connectSlack:
POST /v1/connect/slack•
channelId is no longer required (breaking)• Send
channelId or channelIds (not both)• New
redirect_url (only for channelIds) and new 422 when none connectPOST /v1/imessage/senders/order may now charge the first month before the number is bought when your account spend threshold is below the sender price.This also clarifies failure/retry behavior so clients can handle billing and idempotent retries correctly.
Key updates:
•
402 now also covers first-month card declines (code: payment_required); nothing is ordered — update Billing and order again•
409 can be code: invalid_resource_state when first-month payment isn’t confirmed in time; retry within 1 hour using the same purchaseIntentId (details.purchase_intent_id) to avoid double-charging•
502: provider rejection; nothing charged, or any first month already billed is returned as account creditUse
purchaseIntentId as the idempotency key for safe retries.GET /v1/accounts now supports server-side search, category filtering, and sorting for easier account discovery (especially when combined with pagination).New query params:
•
search - case-insensitive match on username/display name/platform user id, or exact account id•
category - social | ads | communication | blogs•
sort - account | platform | profile | status | connected•
order - asc | desc (default asc)POST /v1/phone-numbers/{id}/whatsapp/request-code to request (or restart) WhatsApp verification for a Zernio-hosted number.This triggers Meta to send a verification code (captured on the number) and can also activate the number immediately if Meta already reports it as verified.
Optional body:
method = SMS | VOICE (omit to let Zernio choose).Success response may include:
alreadyVerified, replaced, newPhoneNumber (when a never-live number is replaced).Possible conflicts:
409 with code such as number_not_whatsapp_eligible or whatsapp_number_in_use; 429 may include retryAt.creative.youtubeVideoIds.This lets you identify the underlying YouTube assets for Google Video campaign ads (video/responsive video) and Demand Gen video ads; when an ad has no image,
thumbnailUrl is derived from the first video’s YouTube thumbnail.New field:
•
creative.youtubeVideoIds (array of strings) — absent on ads without a videoCampaignAnalyticsResponse.campaign.status now returns the platform’s own campaign status (same as platformCampaignStatus on /v1/ads/campaigns and /v1/ads/tree), instead of an “effective” derived status.This means values are now platform-specific (e.g. Google
ENABLED/PAUSED/REMOVED, Meta ACTIVE/PAUSED, ...). For older synced campaigns, it falls back to a child ad’s status as described.If you were checking for
ACTIVE (or other normalized values), update your logic to handle platform-specific statuses from campaign.platform + campaign.status.Connecting a different identity now adds a new account, and reconnecting the same identity refreshes it (ads connections remain one-per-profile). Some selection/assignment endpoints were updated to match this behavior.
Key changes:
•
GET /v1/connect/{platform}: connecting a different account adds it; legacy plans may return 403 platform_account_limit once the per-platform cap is reached•
POST /v1/connect/instagram/select-account: no longer replaces the existing Instagram account on the profile; it adds/refreshes instead•
POST /v1/accounts/{accountId}/gmb-locations/assign: target profileId may already have other Google Business locations; assigning an existing one refreshes it (previously 409 if any GBP connection existed)•
POST /v1/imessage/senders: a profile can now register several iMessage senders; re-registering the same sender refreshes•
POST /v1/rcs/agents: a profile can now hold several RCS agentsNote: WhatsApp number selection conflicts were tightened in
POST /v1/connect/whatsapp/select-phone-number (the one_whatsapp_per_profile conflict is no longer listed there)./v1/accounts now supports a per-profile preview mode via profileIds and perProfile.This lets you request a compact “preview” set of accounts per profile (useful for dashboards/lists) and also returns per-profile totals.
New query params:
•
profileIds — comma-separated profile IDs (up to 50)•
perProfile — integer (1–24); requires profileIds; cannot be combined with page/limitNew response field (only with
profileIds + perProfile):•
profileTotals — map of { profileId: count } for accounts matching the filtersSchema addition:
•
Profile.accountCount — connected account count shown in the profile listPOST /v1/branded-calling/identities has changed.You are now charged $100/identity/month only while the identity is verified. Nothing is charged while the identity is in Zernio review or carrier vetting, and rejected identities are not charged. If you edit a verified identity and it goes back to vetting, the monthly fee is paused until it is verified again.
Applies to identities created with
POST /v1/branded-calling/identities (status starts as requested).GET /v1/changelog.You can page through changelog entries (newest first) and filter by
type and platform. Each entry includes the announcement (message) plus a deterministic OpenAPI diff in changes for automation.Query params:
•
type: new_feature | breaking_change | improvement | deprecation | minor•
platform: slug (e.g. whatsapp)•
before: cursor (date-time)•
limit: 1–100Webhooks can now subscribe to
api.changelog.published by including it in events on POST /v1/webhooks/settings or PUT /v1/webhooks/settings.POST /v1/connect/linkedin/select-organization now supports connecting multiple LinkedIn accounts (personal and/or organizations) from a single sign-in via selections.Breaking change:
accountType is no longer required at the top level (required is now profileId, tempToken, userProfile). Send either accountType (+ selectedOrganization when needed) or selections, not both.Use
selections (1..25 items), each with:•
accountType = personal | organization•
selectedOrganization (for organization)When using
selections, the 200 response returns accounts and failed (instead of a single account), and a new 422 is returned if none of the selections could be connected (with per-item reasons in details.failed).pageIds.Send
pageIds (array, 1–25) instead of pageId to connect several accounts from a single sign-in. With 2+ distinct IDs, the success response returns accounts and failed (instead of account).Applies to:
•
POST /v1/connect/facebook/select-page: pageIds added; pageId is now optional (send pageId or pageIds, not both)•
POST /v1/connect/instagram/select-account: pageIds added; pageId is now optional (send pageId or pageIds, not both)New error case for
pageIds: 422 when none of the requested Pages/accounts could be connected (see details.failed for per-ID reasons).POST /v1/ads/create is now more strict: X no longer accepts creative fields like imageUrl (and also headline, description, video, callToAction) and will return 400.For X, an ad is treated as a promoted post built from
body + linkUrl. If you need an image/video, create/choose a post that contains the media and promote it via POST /v1/ads/boost instead.GET /v1/analytics/dashboard returns an analytics dashboard in a single call (totals, follower growth, per-day series, top posts, recent posts), with optional previous-period comparison.Query params:
•
fromDate (required, YYYY-MM-DD), toDate (required, YYYY-MM-DD, max 366 days)•
profileId (default all), platform (default all)•
compare: previous_period•
topPosts (0–25, default 5), recentPosts (0–25, default 10)Response includes
totals, followers, daily, topPosts, recentPosts, and dataAsOf (plus previousTotals/previousFollowers when compare=previous_period). Requires the Analytics add-on (HTTP 402 if not enabled).You can now include
ageMin/ageMax and gender in TargetingSpec for Search/Display; Google will exclude ranges/genders outside your request.Use:
•
ageMin, ageMax•
gender: all, male, femaleNote:
ageMin/gender are still rejected (400) on Google Performance Max and Demand Gen.TargetingSpec (used in POST /v1/ads/create and POST /v1/ads/targeting/reach-estimate).You can now set:
•
ageMin / ageMax on LinkedIn and X (in addition to Meta/TikTok/Pinterest)•
gender on LinkedIn and X (in addition to Meta/TikTok/Pinterest); values: all, male, female•
languages on TikTok, LinkedIn, and X (in addition to Meta/Google)Note:
ageMin/ageMax and gender are still rejected on Google; languages is still rejected on Pinterest. Unsupported values return 400 with details.GET /v1/connect/{platform}/ads now uses accountId to select the posting account for same-token platforms when a profile has multiple active accounts.If your profile has several active accounts for
platform (facebook, instagram, linkedin, pinterest), pass accountId to choose which one the ads connection should use; with only one active account it can be omitted.New failure case:
409 when the selection is ambiguous (error_reason=ambiguous_account).INVALID_FIELD_VALUE) on create, boost, and targeting update (previously some were documented as ignored).This makes cross-platform targeting stricter: if you send unsupported fields for a given platform, the request will fail instead of being dropped/ignored.
Notable platform restrictions called out in
TargetingSpec:•
ageMin / gender: rejected on Google, LinkedIn, X, OpenAI•
languages: only applied on Meta + Google; rejected on TikTok, LinkedIn, Pinterest, X, OpenAI•
interests: rejected on Google and OpenAIAlso clarified:
userOs and userDevice are Meta only.page_visits.Use it to create Meta "Page visits and followers" campaigns/ads (Traffic objective with Facebook Page destination).
Set
goal to page_visits on:•
POST /v1/ads/campaigns•
POST /v1/ads/createEnum update:
goal now includes page_visits (in addition to existing values)./v1/ads/accounts now returns TikTok advertiser balance details in each item.This lets you display available spend (cash + grant) for TikTok ad accounts;
cashBalance and grantBalance are only present when the connected token has a finance role in the advertiser’s Business Center.New response fields (TikTok only):
•
balance•
cashBalance•
grantBalanceTargetingSpec.behaviors.You can search LinkedIn member behaviors (e.g. Frequent Travelers, Job Seekers) and then pass the returned IDs in your targeting when creating ads.
Use:
•
GET /v1/ads/targeting/search with dimension=behavior• then set
targeting.behaviors (or top-level behaviors on POST /v1/ads/create) to [{ id }] (IDs look like urn:li:memberBehavior:9)When
messagingRestriction is present, you may also receive pausedUntil (date-time) to indicate when held automation DMs will be retried next.New field:
•
messagingRestriction.pausedUntil - next retry time (or null when nothing is held)Available on:
•
GET /v1/accounts/health•
GET /v1/accounts/{accountId}/healthaudience.tapToUnlock.When enabled, Zernio sends the
followGate.message + button to every commenter and only delivers the real DM after they tap, with no follow check.Set:
•
audience.tapToUnlock: true•
followGate.message (optional; used for whenUnknown=verify and tapToUnlock=true)•
followGate.buttonLabel (optional; defaults to “Unlock” for tap-to-unlock)Notes:
•
whenUnknown is ignored when tapToUnlock is true• Cannot be combined with
followerStatus != any or with minFollowerCount (400)• Sending
audience in a PATCH without tapToUnlock clears itbehaviors in POST /v1/ads/create (and in TargetingSpec / saved targeting).You can target TikTok audiences based on video/creator interaction categories (e.g. watched/liked/commented/shared in the last 15 days, or creator interactions). Use
GET /v1/ads/targeting/search with dimension=behavior to discover ids.Set:
•
behaviors: [{ id: "video:..." }] or behaviors: [{ id: "creator:..." }]Note: TikTok behavior targeting uses TikTok placement only.
message.played.This lets you track when an outgoing WhatsApp voice message is played, similar to existing delivery/read status updates.
Subscribe by adding
message.played to events when calling POST /v1/webhooks/settings (or update via PUT /v1/webhooks/settings).The shared delivery-status webhook payload
WebhookPayloadMessageDeliveryStatus.event now includes: message.delivered, message.read, message.played, message.failed.In
GET /v1/inbox/conversations/{conversationId}/messages, deliveryStatus can now be played (in addition to sent, delivered, read, failed, deleted).message.received webhooks now include the unsupported content kind in metadata.unsupported.type.This helps you distinguish what WhatsApp couldn’t deliver (e.g. view-once, polls, edits) instead of only seeing the generic
[Unsupported message] placeholder.When
metadata.unsupported is present, you can now read:•
metadata.unsupported.code•
metadata.unsupported.title•
metadata.unsupported.details•
metadata.unsupported.type (e.g. view_once, poll_creation, group_invite, edit)/v1/inbox/conversations/search now returns additional WhatsApp participant identifiers in each conversation.This helps you anchor identity more reliably across conversations (stable ID) and optionally display the user’s current WhatsApp username.
New fields (WhatsApp only):
•
businessScopedUserId — Meta BSUID (stable identity anchor; present when provided by Meta)•
whatsappUsername — participant username (not stable; captured from inbound messages, may be null until next inbound)This lets you detect forwarded content for moderation/routing, and capture the greeting text Meta showed on Click-to-WhatsApp ads for attribution.
New fields in
WebhookPayloadMessage.metadata:•
forwarded (boolean)•
frequentlyForwarded (boolean)•
referral.welcome_message.text (string)WhatsApp audio attachments now include voice-note metadata:
•
message.attachments[].payload.voice (boolean) — true for WhatsApp voice notes, false for regular audio filesThis helps you anchor WhatsApp users to Meta’s stable BSUID (and optionally show their WhatsApp username) when listing or fetching a conversation.
New fields:
•
GET /v1/inbox/conversations → businessScopedUserId, whatsappUsername•
GET /v1/inbox/conversations/{conversationId} → businessScopedUserId, whatsappUsernameNotes:
•
businessScopedUserId is the recommended stable identity anchor (WhatsApp only)•
whatsappUsername (e.g. jane.shop) is for display and can changereconnectAccountId on GET /v1/connect/{platform}.Use it to refresh a specific existing account and prevent a login from accidentally writing to a different account on the same profile; mismatches are rejected with
reconnect_account_mismatch.New parameter:
•
reconnectAccountId — Zernio account id to refresh (must be same platform and on the same profileId, otherwise 400)Notes:
• In
headless=true flows, keep the returned redirect_url unchanged when calling the selection endpoints so the marker is preserved.• On X (twitter),
reconnectAccountId counts toward the 500-char OAuth state limit.GET /v1/whatsapp/pricing-analytics returns WhatsApp message volume and approximate cost for a connected number, read live from Meta’s pricing_analytics (figures can lag and are approximate).Query params:
•
accountId (required)•
start, end (required, ISO 8601)•
granularity (required): HALF_HOUR | DAILY | MONTHLY• Optional breakdowns via
dimensions: COUNTRY, PHONE, PRICING_CATEGORY, PRICING_TYPE, TIER• Optional filters:
metricTypes (COST, VOLUME), pricingTypes (REGULAR, FREE_CUSTOMER_SERVICE, FREE_ENTRY_POINT), pricingCategories, countryCodesResponse includes
dataPoints with start/end, optional dimension fields, plus volume and cost.messagingRestriction in account health responses has been expanded to cover WhatsApp and now returns both Meta subcodes and WhatsApp Cloud API error codes.This helps you detect messaging blocks/restrictions across Facebook/Instagram and WhatsApp from observed send/delivery failures (not a live probe).
In
GET /v1/accounts/health and GET /v1/accounts/{accountId}/health, messagingRestriction now includes:•
code (WhatsApp Cloud API; nullable)•
subcode (Meta FB/IG; nullable)•
message (nullable)•
firstSeenAt, lastSeenAtWhatsApp codes tracked:
131042, 131031, 368. FB/IG subcodes tracked: 2534122, 1893063, 2534029.whatsapp.account.quality_updated, whatsapp.account.status_updated, whatsapp.account.alert_received.Use these to get notified when Meta changes a connected number’s quality rating / messaging tier, when a WhatsApp Business Account is restricted/reinstated, and when Meta sends account alerts.
Subscribe by including the events in
events when calling POST /v1/webhooks/settings or PUT /v1/webhooks/settings:•
whatsapp.account.quality_updated•
whatsapp.account.status_updated•
whatsapp.account.alert_receivedwhatsapp.contact.identity_changed.This fires when Meta reports a WhatsApp user’s identifier changed (phone number and/or BSUID). Zernio re-keys the inbox conversation/contact channel before emitting the event.
To subscribe, include
whatsapp.contact.identity_changed in events when calling POST /v1/webhooks/settings (or update via PUT /v1/webhooks/settings).Payload key fields:
•
reason: user_changed_number | user_changed_user_id | user_identity_changed | user_id_update•
previous, current: phoneNumber, businessScopedUserId, parentBusinessScopedUserId, whatsappUsername•
contactId, conversationId•
changedAt, timestamppricing and billingConversation on message.sent, message.delivered, message.read, and message.failed.This lets you attribute message costs/categories and detect free entry point billing windows directly from webhook events (fields are absent on non-WhatsApp platforms and may be
null on some statuses).pricing:•
billable•
pricingModel (PMP | CBP)•
category (e.g. marketing, utility, authentication, service)•
type (regular | free_customer_service | free_entry_point)billingConversation:•
id•
expiresAt•
originType (e.g. utility)Applies to:
•
PUT /v1/accounts/{accountId}/facebook-page•
PUT /v1/accounts/{accountId}/linkedin-organization•
PUT /v1/accounts/{accountId}/gmb-locationsHandle
409 with ErrorResponse when you hit profile_platform_conflict./v1/ads/tree now supports incremental syncing and bulk campaign filtering.You can fetch only campaigns that changed since a timestamp (new ad, or changes to ad status/review status/name/budget/creative), which is useful for polling without reloading the full tree.
New/updated query params:
•
updatedSince (ISO 8601 date-time, e.g. 2026-09-30T10:00:00Z)•
campaignId now accepts multiple platform campaign IDs (comma-separated, up to 100): ?campaignId=123,456accountId on some write actions.If a post was published to several accounts on the same platform, you must specify which account’s copy to target; otherwise the request can fail with
409 (ambiguous_account).Changes:
•
POST /v1/posts/{postId}/edit: accountId is required when the post has multiple accounts on that platform; otherwise returns 409•
POST /v1/posts/{postId}/update-metadata (YouTube): accountId is required in direct mode, and also required in post-based mode when the post was published to multiple YouTube accounts; otherwise returns 409•
POST /v1/posts/{postId}/unpublish: new optional accountId to delete a specific account’s copy; returns 409 if ambiguousPlatforms:
•
/edit platform enum: twitter, discord, facebook, reddit, linkedin, telegram, pinterest, googlebusiness, youtube, slack•
/unpublish platform enum: threads, facebook, twitter, linkedin, youtube, pinterest, reddit, bluesky, googlebusiness, telegramGET /v1/ads/keywords.New/updated keywords added in Google should typically appear within ~1 day (first-time sync still populates on the next discovery pass; manual sync/connecting refreshes immediately).
Also updated
429 rate-limit semantics for Google keyword planner + insights endpoints:•
POST /v1/ads/keywords/ideas and POST /v1/ads/keywords/historical-metrics: 429 may indicate a per-user burst limit (15 requests/min) or a Google rate limit•
GET /v1/ads/insights: 429 now calls out Google per-user burst limit vs Google rate limitError details for Zernio Google Ads burst limits are now represented as
details.budgetScope = user (previously could be user or platform).deliveryStatus and deliveryErrorCode on the Verification object.This lets you see what Meta reported for the latest send (e.g., whether the message was delivered/read/failed) and, when failed, the Meta error code.
New fields:
•
deliveryStatus: delivered | read | failed | null•
deliveryErrorCode: integer | nullAlso,
OwnedPhoneNumber now includes metaPoolAddRejectedAt (date-time) when WhatsApp reports the number is registered to another WhatsApp account during connect.MetaCustomerLifecycle validation rules changed for Meta Sales ad sets (ad_set_goal).Audience ID fields are now required/forbidden depending on
strategy, matching Meta’s 400 behavior.Set
strategy to one of: all_customers, new_customers, new_customers_excluding_engagedRules:
•
all_customers: existingCustomerAudienceIds not allowed•
new_customers: existingCustomerAudienceIds required•
new_customers_excluding_engaged: existingCustomerAudienceIds required and engagedAudienceIds required (and engagedAudienceIds not allowed for the other strategies)platformSpecificData.customerLifecycle on PUT /v1/ads/ad-sets/{adSetId}.This lets you set Meta’s Sales ad set goal to optimize for all customers vs acquiring new customers (optionally excluding engaged audiences).
Set
platformSpecificData.customerLifecycle.strategy to:•
all_customers•
new_customers•
new_customers_excluding_engagedOptional (depending on strategy):
•
existingCustomerAudienceIds•
engagedAudienceIds