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.
818 entries
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•
engagedAudienceIdsGET /v1/connect/{platform}/ads: you can now switch a Business Login connection back to the standard login without first completing a Business Login reconnect.If you receive
409 (Business Login connection needs reconnecting), you can either:• Reconnect with
loginMode=business• Or switch to standard login by passing
force=true (or disconnecting the business connection first)Key params:
loginMode (classic | business), force (true | false)timezone.When creating posts with
POST /v1/posts, if you omit timezone and your scheduledFor has no Z/+offset, Zernio will now interpret it using the profile’s timezone (instead of always defaulting to UTC).Set the profile default timezone with:
•
POST /v1/profiles → timezone•
PUT /v1/profiles/{profileId} → timezone (set null to go back to UTC)Bulk CSV uploads also now default
schedule_time to the profile timezone when tz is omitted (else UTC).scopes on GET /v1/connect/{platform}.This lets you ask users only for the permission areas you need; anything not requested won’t be granted and will be missing from the account’s
permissions (e.g. a posting-only account can’t read analytics/inbox until reconnected with more areas).Use
scopes as a comma-separated list:•
posting, analytics, comments, messaging, adsNotes:
• Omit
scopes to request the full permission set (current default behavior).• Rejected with 400
INVALID_FIELD_VALUE on platforms that can’t be reduced: bluesky, telegram, discord, snapchat, whatsapp.Slack start-OAuth also supports this in
GET /v1/connect/slack (start-OAuth mode only) via scopes (on Slack, analytics/comments/ads add nothing; messaging adds inbox/history-related scopes).409 when a tempToken was already used or has expired.This lets you detect a consumed/expired OAuth sign-in and restart the sign-in flow (or use
refreshToken if applicable) instead of treating it as a generic failure.Applies to
POST /v1/connect/linkedin/select-organization with error code oauth_sign_in_consumed.Phone number availability responses now include more detailed area metadata.
In
GET /v1/phone-numbers/availability (and deprecated alias GET /v1/whatsapp/phone-numbers/availability), areaAvailability.* items may now include:•
ndcs (array of all area codes for the city)•
aliases (array of alternate area names)GET /v1/phone-numbers/availability (and the deprecated alias GET /v1/whatsapp/phone-numbers/availability) now returns detailed per-area stock state via areaAvailability.This lets you build an area picker that distinguishes deliverable stock vs pre-orderable areas vs true out-of-stock, using the same 6-hour refreshed data as the dashboard.
New response object:
areaAvailability•
inStock (items: ndc, name, count)•
preOrder (items: ndc, name)•
outOfStock (items: ndc, name, listed)Notes:
•
areaOptions is now documented as equal to areaAvailability.inStock•
soldOutAreas is now described as areaAvailability.preOrder + areaAvailability.outOfStock (kept for older clients)POST /v1/phone-numbers/stock-watches now includes preOrderable in the 200/201 response, so you can tell when the watched area can be bought immediately as a pre-order (submit KYC with areaCode + preOrder: true) while still arming the watch.CtwaAdRequestBody, aligned with POST /v1/ads/create.You can now schedule delivery, choose where budget lives, add ads under an existing campaign, and use advanced targeting options.
New/expanded fields include:
• Creative:
description (link description; not allowed with existing post creatives)• Campaign/ad set:
startDate, budgetLevel (adset | campaign), existingCampaignId• Targeting:
gender (all | male | female), languages, excludedLocations, behaviors, workPositions, workEmployers, workIndustries, incomeTier (top_5 | top_10 | top_10_25 | top_25_50), userOs, userDevice, audienceInclude, audienceExclude, savedTargetingId, targeting, rawTargeting, specialAdCategories, specialAdCategoryCountryNote: when using
adSetId (attach mode) or existingCampaignId, more fields are now explicitly rejected with a 400 per the updated validation rules.DELETE /v1/accounts/{accountId}/tracking-tags/{tagId}/events/{eventId}.TikTok Ads now returns
501 (platform_not_supported) because the upstream delete endpoint responds OK but does not actually remove the event; delete it in TikTok Events Manager instead.Handle
501 for TikTok when calling DELETE /v1/accounts/{accountId}/tracking-tags/{tagId}/events/{eventId}; successful responses still return state = deleted | archived | disabled.platformPostId in CtwaAdRequestBody now supports Instagram media without requiring an Instagram connection (it’s resolved from the media via a Meta ads business-login connection).This makes it easier to reuse existing Instagram posts/reels for CTWA/messaging ads, and you can still customize the chat greeting.
Use:
•
platformPostId (IG or FB post/reel ID)• Optional
welcomeMessage (allowed with existing post references)Reminder: existing post references are still mutually exclusive with fresh creative fields (
headline, body, imageUrl, video) and with each other (platformPostId vs objectStoryId).409 with code=area_pre_order_available when the country requires the number to match the registered address area, but that area has no stock.In this case, nothing is created; you can show the offer to the customer and, if they accept, resend the same request with
preOrder=true to place a carrier-sourced pre-order for that area.Handle
409 code=area_pre_order_available and read details: { areaCode, areaName, estimatedWeeks: "2-4", billedWhenActive: true }. Then retry with preOrder (optionally set areaCode to details.areaCode).Applies to
POST /v1/phone-numbers/kyc and the deprecated alias POST /v1/whatsapp/phone-numbers/kyc.CommerceCapability adds new values: collections.metafields, discounts.codes.This clarifies which operations may return
400 platform_not_supported vs 403 insufficient_permissions, and which features are Shopify-only vs available on WooCommerce.Key notes:
• Collection metafields endpoints (
GET/PUT/DELETE /v1/commerce/collections/{collectionId}/metafields) require collections.metafields; WooCommerce returns 400 platform_not_supported• Discount code management (
POST /v1/commerce/discounts/{discountId}/codes) requires discounts.codes; WooCommerce does not support it• Pages endpoints require
pages.read/pages.write• Navigation endpoints (
/v1/commerce/menus, /v1/commerce/redirects) are Shopify-only and require navigation.read/navigation.write409 failure cases on POST /v1/phone-numbers/kyc (and the deprecated alias POST /v1/whatsapp/phone-numbers/kyc).Pre-orders can now happen automatically even without sending
preOrder when the country/type has no deliverable stock, or when a geographic-match country requires an address-covered area that’s out of stock. The response preOrder indicates when this happened.Handle new/expanded
409 error codes:•
area_code_unavailable - requested areaCode (or address-covered area) has no deliverable inventory and can’t be pre-ordered•
country_out_of_stock - the whole country/type pool has nothing deliverable and can’t be pre-orderedWhen inventory is temporarily held due to recent WhatsApp registration failures,
details.undeliverableUntil is returned as an ISO timestamp in the error details.POST /v1/ads/boost and POST /v1/ads/create now have clarified, consistent semantics for status when creating ads.When you send
status: PAUSED, Zernio pauses only the top-most object created by the request and leaves everything below it switched on, so a single resume of that object brings the new tree live (and existing parents are never touched).Key behavior:
• New campaign created: campaign is paused; ad set + ad are on
• With
existingCampaignId: new ad set is paused; its ad is on• With
adSetId: the new ad itself is pausedValues:
status = ACTIVE | PAUSEDAlso note:
Ad.configuredStatus is the platform-level on/off toggle for the ad itself; on a paused create it may still read ACTIVE while delivery status reads paused (because the pause is held at the campaign/ad set).Endpoints:
•
GET /v1/imessage/sandbox/contacts — returns the sandbox line (sandbox.accountId, sandbox.handle, sandbox.contactLimit) and your contacts•
POST /v1/imessage/sandbox/contacts — add a contact with handle (E.164 phone like +15551234567 or Apple ID email)•
DELETE /v1/imessage/sandbox/contacts/{contactId} — remove a sandbox contactContact fields:
•
status: pending | active• Activate by sending
joinText from the contact to the sandbox line (or use joinLink)• Reply window is based on
lastInboundAt (24 hours after each inbound message)Start by fetching store capabilities so you can detect required permissions up front.
Key endpoints:
• Store + capabilities:
GET /v1/commerce/store (accountId) → capabilities, missingCapabilities, grantPermissionsUrl• Products:
GET /v1/commerce/products (accountId, limit, cursor, status, query, collectionId), POST /v1/commerce/products, PATCH /v1/commerce/products/{productId}, POST /v1/commerce/products/state (action: activate/deactivate/archive/delete), POST /v1/commerce/products/{productId}/price• Collections:
GET/POST /v1/commerce/collections, PATCH/DELETE /v1/commerce/collections/{collectionId}, POST /v1/commerce/collections/{collectionId}/products, POST /v1/commerce/collections/{collectionId}/reorder• Inventory:
GET /v1/commerce/inventory (accountId, productId), POST /v1/commerce/products/{productId}/inventory (mode: set/adjust)• Discounts:
GET/POST /v1/commerce/discounts, PATCH/DELETE /v1/commerce/discounts/{discountId}, POST /v1/commerce/discounts/{discountId}/state, POST /v1/commerce/discounts/{discountId}/codes• Meta catalog sync:
POST /v1/commerce/catalog-syncs (accountId, catalogAccountId, catalogId), POST /v1/commerce/catalog-syncs/{syncId}/runWebhooks: you can now subscribe to product lifecycle events via
POST /v1/webhooks/settings / PUT /v1/webhooks/settings with:•
commerce.product.created•
commerce.product.updated•
commerce.product.deletedBreaking change (Ads): campaign/ad set status toggles now write only the campaign/ad set switch and no longer cascade to child ad sets/ads.
• Campaign:
PUT /v1/ads/campaigns/{campaignId}/status• Bulk:
POST /v1/ads/campaigns/bulk-status• Ad set:
PUT /v1/ads/ad-sets/{adSetId}/status and PUT /v1/ads/ad-sets/{adSetId}If you relied on cascade resume/pause, also toggle children explicitly (e.g.
PUT /v1/ads/ad-sets/{adSetId}/status, PUT /v1/ads/{adId}/status)./v1/whatsapp/templates can now return 502 when Meta rejects the request or is unreachable.This makes Meta-side failures explicit so you can distinguish them from validation errors and apply retries/backoff.
Handle:
•
502 - Meta rejected the request or was unreachable (Meta 4xx statuses are forwarded as-is)Requests that include
TargetingSpec.behaviors for TikTok ads will now be rejected with 400.Impacted surfaces:
•
POST /v1/ads/create (via targeting.behaviors or top-level behaviors)•
GET /v1/ads/targeting/search with dimension=behavior (now Meta-only)If you need TikTok audience refinement, use
interests, geo (countries/regions/cities/zips/metros), ageMin/ageMax, gender (all/male/female), and incomeTier (top_5/top_10/top_10_25/top_25_50) where supported.This lets you read/set/remove the page’s Get Started postback payload (required by Meta before a persistent menu can be shown).
New endpoints:
•
GET /v1/accounts/{accountId}/messenger-get-started → returns data (or null) with payload•
PUT /v1/accounts/{accountId}/messenger-get-started with payload (1–1000 chars), e.g. GET_STARTED or zernio:workflow:<workflowId>•
DELETE /v1/accounts/{accountId}/messenger-get-started (returns 409 if a persistent menu is still set)Also updated:
PUT /v1/accounts/{accountId}/messenger-menu now returns 409 if the page has no Get Started button set.GET /v1/accounts/{accountId}/tracking-tags/{tagId}/stats now includes both on-site Pixel and Conversions API events in the recent-events stream. Check api_channel: pixel_sdk (includes openai::sdk_init) and server_to_server (Conversions API).GET /v1/accounts/{accountId}/tracking-tags/{tagId}/events clarifies OpenAI Ads attribution windows: clickWindowDays and viewWindowDays (0 = off).Use
live=true to read switches from the platform now, store them, and return when the read happened (or null if the live read failed and stored values were returned).New/updated parameters:
•
GET /v1/ads/campaigns: live=true (requires limit<=20)•
GET /v1/ads/ad-sets: live=true (requires campaignId or adSetId; reads live for first 20 rows)•
GET /v1/ads/{adId}: live=trueNew response fields:
• Campaigns:
statusReadAt (only when live=true)• Ad sets list:
statusReadAt per row (only when live=true)• Ad details:
statusReadAt (only when live=true)Status write behavior is now more immediately consistent:
•
PUT /v1/ads/campaigns/{campaignId}/status, POST /v1/ads/campaigns/bulk-status, and PUT /v1/ads/ad-sets/{adSetId}/status now read child ad switches live (up to 20), then re-read and store switches/statuses after the write so a subsequent GET reflects what the platform reports right away.This helps avoid accidental off-by-timezone starts/ends when using
startTime/endTime (duplicate) and startDate/endDate (create/boost).Key rules:
•
POST /v1/ads/campaigns/{campaignId}/duplicate: startTime/endTime are read in the ad account timezone on Meta/TikTok; LinkedIn reads no-offset values as UTC. Date-only endTime runs to 23:59:59 local.•
POST /v1/ads/ad-sets/{adSetId}/duplicate (Meta): startTime/endTime no-offset values are read in the ad account timezone; date-only endTime runs to 23:59:59 local.•
POST /v1/ads/boost: startDate/endDate no-offset values are read in the ad account timezone on Meta/TikTok/X/Pinterest; date-only endDate runs to 23:59:59 local.•
POST /v1/ads/create: startDate/endDate parsing is now specified per platform (account timezone on Meta/TikTok/X/Pinterest/OpenAI; Google uses account-local calendar days; LinkedIn reads no-offset values as UTC).startDate / endDate handling was updated for ads scheduling.For Meta (and now also TikTok on boosts), a timestamp without a timezone offset (e.g.
YYYY-MM-DD, YYYY-MM-DD HH:MM:SS, YYYY-MM-DDTHH:MM:SS) is interpreted in the ad account timezone instead of UTC.Applies to:
•
PUT /v1/ads/ad-sets/{adSetId} via platformSpecificData.startDate / platformSpecificData.endDate (Meta)•
POST /v1/ads/boost via startDate / endDate (Meta + TikTok)•
POST /v1/ads/create via startDate / endDate (Meta + TikTok)If you need an exact instant, always send ISO 8601 with an offset (e.g.
2026-06-10T09:00:00Z or 2026-06-10T09:00:00-05:00).PUT /v1/ads/ad-sets/{adSetId}/status now writes the ad set’s own on/off switch on every supported platform (instead of emulating via child ads on some platforms).The response no longer returns
message when nothing was written; callers should rely on status, updated, skipped, and skippedReasons.Set
status to active or paused (with required platform).PUT /v1/ads/{adId}/status now returns the post-write platform read-back:•
status (delivery status)•
configuredStatus (the ad’s own switch; may be null where unsupported)Skips are now based on the ad’s own switch already matching the target (not the rolled-up delivery
status).You can distinguish ancestor-cascaded delivery
status from the entity’s own toggle:• Ad:
configuredStatus now maps TikTok operation_status (ENABLE → ACTIVE, DISABLE → PAUSED)• Campaign rollups:
platformCampaignStatus now includes TikTok campaign operation_status (ENABLE/DISABLE)• Ad sets list:
platformAdSetStatus now documents TikTok ad group operation_status (ENABLE/DISABLE)Also clarified scheduling behavior:
startDate/endDate are stored/read as UTC instants, and TikTok inputs without an offset are interpreted in the ad account timezone.platform: xads) on the existing tracking-tag endpoints.You can now list/read/create X Pixels and manage X web event tags via the same
/v1/accounts/{accountId}/tracking-tags API surface.Key behavior for X Ads:
• List pixels:
GET /v1/accounts/{accountId}/tracking-tags (optional adAccountId to scope). TrackingTag.id is the X ad account id (base36, e.g. 18ce54d4x5t); siteTagId is the pixel id embedded on the site.• Create pixel:
POST /v1/accounts/{accountId}/tracking-tags with adAccountId (X ad account id). One pixel per ad account; if it already exists, X returns 409.• Read pixel:
GET /v1/accounts/{accountId}/tracking-tags/{tagId} where tagId is the X ad account id; returns code, siteTagId, and events.• Update pixel:
PATCH /v1/accounts/{accountId}/tracking-tags/{tagId} supports firstPartyCookieStatus = first_party_cookie_enabled | first_party_cookie_disabled.• Events:
POST/PATCH/DELETE /v1/accounts/{accountId}/tracking-tags/{tagId}/events now work for X web event tags. Create supports type (X enum: ADDED_PAYMENT_INFO, ADD_TO_CART, ADD_TO_WISHLIST, CHECKOUT_INITIATED, CONTENT_VIEW, CUSTOM, DOWNLOAD, INSTALL, LANDING_PAGE_VIEW, LOGIN, PRODUCT_CUSTOMIZATION, PURCHASE, SEARCH, SESSION, SIGN_UP, SITE_VISIT, START_TRIAL, SUBSCRIBE) plus clickWindowDays and viewWindowDays.• Stats:
GET /v1/accounts/{accountId}/tracking-tags/{tagId}/stats returns X web event tag health rows (no per-event counts).platform=tiktokads) across the tracking-tags API (pixels, sharing, events, and stats).You can now manage TikTok Pixels via:
•
GET /v1/accounts/{accountId}/tracking-tags (optionally adAccountId=<numeric advertiser_id>)•
GET /v1/accounts/{accountId}/tracking-tags/{tagId} (searches advertisers if adAccountId omitted; TikTok has no lastFiredTime)•
POST /v1/accounts/{accountId}/tracking-tags (adAccountId=numeric advertiser_id; name must be unique, max 40 chars; no delete API)Sharing is supported via Business Center:
•
GET|POST|DELETE /v1/accounts/{accountId}/tracking-tags/{tagId}/shared-accounts (TikTok pixels must be Business Center assets; otherwise 400)Conversion events and stats:
•
GET|POST /v1/accounts/{accountId}/tracking-tags/{tagId}/events (TikTok event list may lag 2–4 hours)•
PATCH|DELETE /v1/accounts/{accountId}/tracking-tags/{tagId}/events/{eventId} (update supports name, defaultValue, currency; delete is hard delete but fails if bound to an ad group)•
GET /v1/accounts/{accountId}/tracking-tags/{tagId}/stats (TikTok per-event daily counts; defaults last 7 days; max 30 days; aggregation not accepted)Note: TikTok connections authorized before the Pixel Management permission (about 2026-05-28) may return
403 until reconnected.You can now send server-side conversion events to Pinterest, list Pinterest conversion destinations (ad accounts), and read Pinterest event quality metrics.
Use
POST /v1/ads/conversions with:•
accountId = a pinterestads SocialAccount•
destinationId = numeric Pinterest ad account id (e.g. 549755885175)• Pinterest requirements: each event needs
user.email OR (user.ipAddress + user.userAgent)• Pinterest click id:
user.clickIds.epik (sent as click_id)•
testCode on Pinterest sends with test=true•
consent.adUserData: DENIED sets Pinterest opt_outEvent quality now supports Pinterest via
GET /v1/ads/conversions/quality:•
accountId = metaads or pinterestads•
destinationId = Meta pixel/dataset id OR Pinterest numeric ad account idConversion destinations now include Pinterest via
GET /v1/accounts/{accountId}/conversion-destinations (platform enum now includes pinterestads).