Zernio
Zernio
QuickstartBuild a PlatformSDKsCLIMCPWebhooksWorkflowsGuidesSecurityGlossaryPricingBillingChangelogRefer & earn
Dashboard
llms.txtOpenAPI
OverviewPlatformsAPI ReferenceResources

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.

RSS for this viewJSONWebhook

844 entries

October 7, 2026Improvement
Error handling updated for Pinterest boards and YouTube playlists listing endpoints.

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 request

YouTube playlists: GET /v1/accounts/{accountId}/youtube-playlists
• 401 - missing/invalid API key (missing_credentials / invalid_credentials) or YouTube token revoked/expired (reconnect_required)
October 7, 2026Improvement
WhatsApp template sends now return a clearer 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}/messages

If 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.
October 7, 2026New feature
New endpoint: 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.
October 7, 2026Improvement
CTWA ad creation responses now may include 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
• CtwaMultiResponse
October 7, 2026Improvement
Outbound call origination now supports a configurable ring timeout via ringTimeoutSeconds 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.
October 7, 2026New feature
Google support added to two ads endpoints.

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 verbatim
Note: 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”
October 7, 2026Breaking change
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)
• primaryForGoal

PATCH /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).
October 7, 2026New feature
Google Ads now supports shared campaign budgets.

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 request

Only available for Google Ads; other platforms return 501.
October 7, 2026Breaking change
iMessage sender ordering can now return 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, failed

Note: 409 no longer includes the prior “retry within the hour with purchaseIntentId” case; use purchaseIntentId for idempotent retries as before.
October 7, 2026Improvement
GET /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_hints

targeting can still be null for ad sets not re-synced yet (or if targeting reads have consistently failed).
October 7, 2026New feature
Google Ads now supports Target Impression Share bidding across portfolio strategies and Search campaigns.

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): maxCpc

Target 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.
October 7, 2026New feature
Google Ads campaign targeting now supports excluded (negative) locations on campaign targeting read/write.

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 400
October 7, 2026New feature
GET /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 null
October 7, 2026Improvement
PlatformAnalytics 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 platforms
October 7, 2026New feature
Google Ads campaign/ad-group assets now support image assets.

You 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 → images

Notes:
• 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
October 7, 2026Improvement
Facebook page selection now returns 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).
October 6, 2026Improvement
GET /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)
October 6, 2026Improvement
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).
October 6, 2026Breaking change
Several Google Ads DELETE endpoints now take their fields in the query string (a JSON body with the same fields is still accepted; if both are sent, the query string is ignored).

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, assetId

Also:
• 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)
October 6, 2026Improvement
Comment automation responses now clarify that on reply-only platforms (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/youtube

Also, 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.
October 6, 2026New feature
ManyChat-style automation primitives are live (API 1.223.0). This completes the earlier entry on Message Requests, Messenger ice breakers and the workflow trigger endpoint.

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)
October 6, 2026Improvement
OAuth connect now supports account mismatch protection on 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.platformUserId
October 6, 2026Improvement
Meta carousel ads now return per-card creative details via creative.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, videoId

Only present for Meta carousel ads; absent for non-carousel ads/other platforms. Older synced carousels will populate carouselCards on their next sync.
October 6, 2026New feature
Whop Ads is now supported for tracking tags (Whop Pixel) via the tracking-tags API.

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}/stats

Accounts health filtering also adds platform=whopads on GET /v1/accounts/health.
October 6, 2026New feature
Inbox now supports Facebook/Instagram Message Requests: list them with 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-breakers
Ice 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.
October 6, 2026New feature
New endpoint: 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).
October 6, 2026New feature
Video media items now support subtitle/closed-caption tracks via 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).
October 6, 2026Improvement
TikTok Commercial Music: when attaching CML audio, prefer the track’s 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.musicSoundId
October 6, 2026Improvement
LinkedIn boosts/ads created with adStatus: 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 adStatus

Key field: adStatus (ACTIVE | PAUSED)
October 6, 2026Breaking change
DELETE /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 | PAUSED

These override status for their respective level (and are rejected with adSetId/existingCampaignId where that level isn’t created).
October 6, 2026New feature
Messaging ads now support carousel creatives on 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
October 6, 2026Improvement
POST /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 from
October 5, 2026Improvement
comment.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.received
October 5, 2026Improvement
Google Ads keyword endpoints now return more specific 429 rate limit details for Zernio burst limits.

When 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 accounts

Applies to POST /v1/ads/keywords/ideas and POST /v1/ads/keywords/historical-metrics (429 responses).
October 5, 2026Breaking change
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[].follows
October 5, 2026New feature
New endpoint GET /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).
October 5, 2026New feature
Meta ad video uploads now support async processing, with a new status endpoint and webhook.

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: processing

Webhook: subscribe to ad.video.processed (fires for async: true uploads) with video.status: ready | error.
October 5, 2026New feature
You can now filter 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).
October 5, 2026Improvement
POST /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.
October 5, 2026Breaking change
GET /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 it

PUT /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.
October 5, 2026Improvement
Rate limiting is now standardized across Meta ads endpoints, and 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: ErrorResponse

Applies 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.
October 5, 2026New feature
New endpoint: 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.
October 4, 2026New feature
New endpoint: 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.
October 4, 2026New feature
Custom audiences now support new engagement audience types: 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/pinIds

Also 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).
October 4, 2026Improvement
GET /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 second
October 3, 2026Improvement
igReelsAvgWatchTime 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.igReelsVideoViewTotalTime
October 3, 2026New feature
Meta attach mode (adSetId) 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
October 3, 2026New feature
X ads created via 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 X
October 3, 2026New feature
Google ads targeting now supports interests via targeting.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 Google
October 3, 2026New feature
TikTok now supports Smart Targeting on ad groups via smartTargeting.

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).
October 3, 2026Improvement
GET /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)
October 3, 2026Improvement
GET /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).
October 3, 2026Improvement
GET /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
• configReadAt
October 2, 2026New feature
New endpoint: GET /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 content

TikTok-only: other platforms return 501. 404 if the ad isn’t found, has no TikTok ad id yet, or TikTok has no review record.
October 2, 2026Improvement
GET /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).
October 2, 2026Breaking change
Google Business Profile, Discord, and Slack connect endpoints now support batch connecting multiple destinations in one call.

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 connect

Discord: 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 connect

Slack: 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 connect
October 2, 2026Improvement
Ordering iMessage senders via POST /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 credit

Use purchaseIntentId as the idempotency key for safe retries.
October 2, 2026Improvement
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)
October 2, 2026Improvement
Error responses now may include a documentation URL via docUrl in ErrorResponse.

When present, docUrl points to a page describing how to resolve the specific error.

New field:
• docUrl (string)
October 2, 2026New feature
New endpoint: 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.
October 2, 2026Improvement
Google ads now return YouTube video IDs on video-based ads via 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 video
October 2, 2026Breaking change
CampaignAnalyticsResponse.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.
October 1, 2026Breaking change
Profiles can now hold multiple accounts per platform in the connect flow.

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 agents

Note: 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).
October 1, 2026New feature
GET /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/limit

New response field (only with profileIds + perProfile):
• profileTotals — map of { profileId: count } for accounts matching the filters

Schema addition:
• Profile.accountCount — connected account count shown in the profile list
October 1, 2026Improvement
Billing for branded calling identities created via POST /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).
October 1, 2026Improvement
GET /v1/changelog no longer requires an API key and is now rate-limited (120 requests/min per address).

If you exceed the limit, the endpoint can return 429 (RateLimited), so make sure your client handles 429 responses (e.g., backoff/retry).
October 1, 2026New feature
API changelog is now available via 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–100

Webhooks can now subscribe to api.changelog.published by including it in events on POST /v1/webhooks/settings or PUT /v1/webhooks/settings.
October 1, 2026Breaking change
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).
October 1, 2026Improvement
Facebook and Instagram connect flows now support connecting multiple Pages/accounts in one request via 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).
October 1, 2026Breaking change
X ads creation via 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.
October 1, 2026New feature
New endpoint 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).
October 1, 2026Improvement
Google ads targeting now supports age and gender on Google Search/Display.

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, female

Note: ageMin/gender are still rejected (400) on Google Performance Max and Demand Gen.
October 1, 2026Improvement
Pinterest ads now support language targeting via targeting.languages.

You can restrict the audience by language on Pinterest (previously this field was rejected with 400).

Use targeting.languages as an array of language codes (e.g. ["en"]).
October 1, 2026Improvement
Ad targeting now supports age, gender, and languages on more platforms via 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.
October 1, 2026Breaking change
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).
October 1, 2026Breaking change
Ad targeting fields that a platform can’t honor are now rejected (400 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 OpenAI

Also clarified: userOs and userDevice are Meta only.
October 1, 2026New feature
Meta ads now support a new goal: 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/create

Enum update: goal now includes page_visits (in addition to existing values).
October 1, 2026Improvement
GET /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
• grantBalance
September 30, 2026New feature
LinkedIn now supports behavior targeting via TargetingSpec.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)
September 30, 2026Improvement
Account health responses now include automation DM retry timing when Meta temporarily refuses messaging.

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}/health
September 30, 2026New feature
Comment-to-DM automations now support tap-to-unlock gating for Instagram via audience.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 it
September 30, 2026New feature
TikTok now supports behavior targeting via behaviors 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.
September 30, 2026New feature
Webhooks now support voice message playback status via the new event 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).
September 30, 2026Improvement
WhatsApp 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)
September 30, 2026Improvement
GET /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)
September 30, 2026Improvement
WhatsApp inbound message.received webhooks now include forwarded indicators and CTWA welcome message context.

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 files
September 30, 2026Improvement
WhatsApp conversations now include participant identity/display fields in the Inbox conversation APIs.

This 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, whatsappUsername

Notes:
• businessScopedUserId is the recommended stable identity anchor (WhatsApp only)
• whatsappUsername (e.g. jane.shop) is for display and can change
September 30, 2026Improvement
OAuth connect now supports safe reconnect via reconnectAccountId 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.
September 30, 2026New feature
New endpoint 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, countryCodes

Response includes dataPoints with start/end, optional dimension fields, plus volume and cost.
September 30, 2026Improvement
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, lastSeenAt

WhatsApp codes tracked: 131042, 131031, 368. FB/IG subcodes tracked: 2534122, 1893063, 2534029.
September 30, 2026New feature
Webhooks now support new WhatsApp account-level events: 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_received
September 30, 2026New feature
Webhooks now support a new WhatsApp event: whatsapp.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, timestamp
September 30, 2026New feature
WhatsApp webhooks now include Meta billing details via pricing 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)
September 30, 2026Improvement
Switching destinations can now return 409 Conflict when the target is already connected by another account on the same profile.

Applies to:
• PUT /v1/accounts/{accountId}/facebook-page
• PUT /v1/accounts/{accountId}/linkedin-organization
• PUT /v1/accounts/{accountId}/gmb-locations

Handle 409 with ErrorResponse when you hit profile_platform_conflict.
September 30, 2026New feature
GET /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,456
September 30, 2026Breaking change
Multi-account posts now require disambiguation via accountId 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 ambiguous

Platforms:
• /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, telegram
September 30, 2026Improvement
Google Ads keyword data now refreshes daily instead of weekly on GET /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 limit

Error details for Zernio Google Ads burst limits are now represented as details.budgetScope = user (previously could be user or platform).
September 30, 2026Improvement
WhatsApp OTP verifications now return delivery telemetry via 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 | null

Also, OwnedPhoneNumber now includes metaPoolAddRejectedAt (date-time) when WhatsApp reports the number is registered to another WhatsApp account during connect.
September 30, 2026Breaking change
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_engaged

Rules:
• 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)
September 30, 2026New feature
Meta ad set updates now support Customer Lifecycle Strategy via 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_engaged

Optional (depending on strategy):
• existingCustomerAudienceIds
• engagedAudienceIds
Was this page helpful?

Payment method required

What a 402 payment_required response means, which requests trigger it, and how the user adds a card without leaving the dashboard.

Refer & earn

Earn 20% recurring commission for 12 months when you refer new customers to Zernio.