Zernio | Developer News
676 subscribers
1 photo
1 video
475 links
This channel is for recurrent updates and announcements on https://zernio.com.

Zernio is a Social API for Posting and Analytics on 14 platforms.
Download Telegram
WhatsApp now supports re-registering a connected number on Meta Cloud API, and the credentials connect flow can include a 2-step verification PIN.

If a number has its own 2-step PIN, Meta can reject registration (error 133005) and sends may fail with misleading permission errors. You can now pass the PIN during connect, or re-run registration later.

Use:
POST /v1/connect/whatsapp/credentials with optional pin (6 digits, ^\d{6}$)
• New: POST /v1/accounts/{accountId}/whatsapp/register with optional pin

Connect response may include registrationWarning when the account was created but Cloud API registration was rejected (number can’t send until resolved).

Inbox comments endpoints now also return platform/upstream error responses you may want to handle: 403 (platform permission), 429 (rate limited), 502 (upstream platform error) on GET/POST/DELETE /v1/inbox/comments/{postId}.

View documentation
You can now manage OAuth connected apps for the authenticated user: list authorized clients and revoke their access.

List connected apps with GET /v1/me/connected-apps. Each item includes clientId, clientName, redirectHost, scopes, authorizedAt, lastUsedAt, tokenCount.

Revoke an app with DELETE /v1/me/connected-apps/{clientId} (idempotent). Response includes revoked, revokedTokens, invalidatedCodes.

Auth note: requires a session or full-scope API key; profile-scoped API keys and OAuth access tokens get 403 (insufficient_permissions).

View documentation
Create/duplicate ads endpoints now support safe retries via an Idempotency-Key header.

This prevents accidental duplicate campaigns/ad sets/ads when your client retries after timeouts or network errors.

Now supported on:
POST /v1/ads/campaigns
POST /v1/ads/campaigns/{campaignId}/duplicate
POST /v1/ads/ad-sets/{adSetId}/duplicate
POST /v1/ads/{adId}/duplicate

Behavior:
• same Idempotency-Key + same body → replay original 2xx response
• same key + different body → 422
• key still processing → 409

Also: metrics conversions is now a number (can be fractional) in GET /v1/ads/timeline and the AdMetrics schema (attribution splitting + Google modeled conversions).

View documentation
GET /v1/accounts/{accountId}/linkedin-mentions now returns 403 when LinkedIn blocks person-mention lookup due to missing company page posting privileges.

This lets you distinguish “insufficient LinkedIn org/page role” from invalid input (400) or not found (404), and show a clearer remediation message.

New response:
403 - account has no organization role that can resolve person mentions (error message indicates needing Admin/Content Admin on a page, or mentioning an organization instead)

View documentation
Facebook post options have been reorganized: draft/carousel/colored-background text settings are now grouped under facebookSettings.

This makes Facebook-specific options consistent and clarifies which fields belong under platformSpecificData vs nested settings.

Move these fields under facebookSettings (root-level or per-platform):
draft
carouselCards
carouselLink
textFormatPresetId

Applies to POST /v1/posts and PUT /v1/posts/{postId}. Also note: firstComment is skipped when facebookSettings.draft is true.

View documentation
POST /v1/contacts/bulk request requirements and account/platform resolution have changed.

You can now do a CRM-only import without channels, but when importing channel identifiers you must attach them to an account; duplicates are still skipped, but any new tags are merged onto the existing contact.

Key changes:
• Top-level required fields are now profileId and contacts (no longer always requiring accountId/platform)
accountId is required when contacts include platformIdentifier (or a row-level accountId); otherwise the request returns 400
platform is ignored when accountId is set; the platform is derived from the resolved account, and a mismatch can 404 as account not found

View documentation
Meta ads campaign create/delete behavior has been tightened and now supports deleting “empty” campaign shells.

On POST /v1/ads/campaigns, budgetAmount is explicitly in WHOLE currency units (not cents), and bidStrategy now requires a campaign budget (budgetAmount + budgetType = daily | lifetime). Sending bidStrategy without a campaign budget is now a 400, and invalid bid states (e.g. missing bidAmount for LOWEST_COST_WITH_BID_CAP/COST_CAP) are rejected up front.

On DELETE /v1/ads/campaigns/{campaignId}, you can now delete a campaign with zero ads by including accountId in the body (alongside platform = facebook | instagram). accountId is only required for empty campaigns.

On POST /v1/ads/create, budgetAmount is also clarified as WHOLE currency units (not cents) to avoid Meta minor-unit mixups.

View documentation
GET /v1/ads/campaigns now supports listing Meta empty campaigns (campaigns with adCount: 0), which were previously invisible because campaigns are aggregated from ad documents.

Use includeEmpty=true to include these campaigns (metrics will be zeroed). Requires accountId and adAccountId when includeEmpty=true.

PUT /v1/ads/campaigns/{campaignId} can now update an empty Meta campaign by sending accountId in the request body (otherwise it may 404 due to no local ad docs). The response will return updated: 0 for empty campaigns.

View documentation
POST /v1/contacts/bulk now treats missing platformIdentifier as a per-contact failure when importing in channel mode.

If you send a top-level accountId, each contact is expected to include platformIdentifier; rows missing it are rejected individually and returned in errors[] under an HTTP 200 (the whole import no longer fails with HTTP 400 for that case).

Key behavior:
• Channel mode: accountId set + contact missing platformIdentifier → HTTP 200 with errors[]
• Request-level 400 still applies for missing required name, or when a row includes channel data but there is no top-level accountId to attach it to

View documentation
1
GET /v1/ads/accounts now returns additional platform status details for ad accounts.

This lets you distinguish platform-specific states (e.g., Meta disabled vs. billing vs. review) and show better “why can’t I run ads?” messaging instead of relying only on selectable.

New fields in accounts[]:
accountStatus (platform status; type varies by platform)
disableReason (Meta only; integer code when accountStatus indicates disabled)
approvalStatus (X only; observed: ACCEPTED, PENDING, REJECTED)

Note: LinkedIn continues to use status (filtered to active accounts).

View documentation
GET /v1/contacts now supports filtering by multiple tags via a new tags query parameter.

Use tags as a comma-separated list to match contacts that have any of the provided tags.

New parameter: tags (comma-separated, OR match). Example: tags=vip,lead,partner

View documentation
LinkedIn ads now return delivery diagnostics and effective bidding details via new fields on Ad and GET /v1/ads/tree nodes.

This lets you distinguish “configured active” vs “actually serving” and understand why a LinkedIn campaign/creative is not delivering (holds, review, budget, etc.).

New fields:
Ad.costType (LinkedIn only): CPM | CPC | CPV
Ad.servingStatuses (LinkedIn only): array of serving/hold reasons (e.g. RUNNABLE, ACCOUNT_TOTAL_BUDGET_HOLD)
Ad.creative.isServing (LinkedIn only): true | false | null
Ad.creative.servingHoldReasons (LinkedIn only): creative-level hold reasons (e.g. UNDER_REVIEW, REJECTED)

Also added to tree responses:
AdTreeAdSet.costType, AdTreeAdSet.servingStatuses

Note: for LinkedIn, bidAmount represents the campaign’s effective unitCost and can be 0 (a real, delivery-stopping value), independent of bidStrategy.

View documentation
PostAnalytics now includes videoDurationSeconds (nullable integer).

This exposes the video length (currently Instagram Reels only) so you can estimate retention when combined with igReelsAvgWatchTime (ms).

New field:
videoDurationSeconds - video length in seconds; null when unknown (non-video/other platforms, or when Instagram doesn’t expose the media URL, e.g. some copyrighted-audio reels).

View documentation
Sending inbox messages now supports idempotent retries via Idempotency-Key on POST /v1/inbox/conversations/{conversationId}/messages.

This prevents duplicate sends when you retry after timeouts: same key + same body replays the original response (with Idempotent-Replayed: true) instead of sending again. Works for both application/json and multipart/form-data requests; keys are retained for 24 hours.

Use:
Idempotency-Key (header)

New error responses:
409 - Idempotency key still in flight
422 - Idempotency key reused with a different body

View documentation
WhatsApp now supports Meta Direct Send utility messages via category on inbox messaging endpoints.

Eligible WhatsApp Business Accounts can start or send a business-initiated utility text message without an approved template (Meta matches/auto-creates a template asynchronously). If the WABA isn’t eligible, the request fails and you must use an approved template.

Use category:
POST /v1/inbox/conversations: set category=utility with message and no templateName (WhatsApp only)
POST /v1/inbox/conversations/{conversationId}/messages: set category=utility to send as a business-initiated utility message (WhatsApp only)

Notes:
category enum: utility
category cannot be combined with templateName (create-conversation) or template (send-message)
category is accepted on application/json only (not multipart/form-data)

View documentation
POST /v1/sms/sender-ids no longer enforces global uniqueness for alphanumeric sender IDs.

The same senderId can now be registered by multiple workspaces, so creating a sender ID should no longer fail due to another workspace already having it.

Changes:
• Removed 409 (sender_id_taken) conflict response
• Increased active sender ID limit in 403 from 5 to 1,000 (sender_id_limit_reached)

Request remains: senderId

View documentation
In PUT /v1/posts/{postId}, platforms[].platformSpecificData update behavior is now explicitly defined.

A Settings namespace (e.g. facebookSettings, tiktokSettings) that is omitted from the request is preserved from the stored post. If you send the key, it replaces the entire namespace (it is not deep-merged).

Key field: platforms[].platformSpecificData

View documentation
Updating a post now preserves per-platform settings unless you explicitly send them in platforms[].platformSpecificData.

A Settings namespace (e.g. facebookSettings, tiktokSettings) omitted from the request is kept from the stored post. If you include the key, it replaces the entire namespace (no deep-merge).

Also, port-in now supports Brazil by allowing endUser.countryCode = BR in POST /v1/phone-numbers/port-in (enum now includes US, CA, GB, ES, DE, FR, NL, AU, BR).

View documentation
PUT /v1/posts/{postId} now requires platforms when sending root-level tiktokSettings or facebookSettings.

If you include tiktokSettings or facebookSettings in an update request without a platforms array, the API will return 400. Include the target platforms (with platform + accountId) in the same request when updating these settings.

View documentation
Connect flow redirects now include the created social account id in the success redirect_url for these endpoints:
POST /v1/connect/facebook/select-page
POST /v1/connect/googlebusiness/select-location
POST /v1/connect/pinterest/select-board
POST /v1/connect/snapchat/select-profile

This lets you reliably capture the connected account immediately after OAuth without extra lookup.

Look for accountId in the returned redirect_url query params (e.g. ...&accountId=...).

View documentation
Ad.creative.thumbnailUrl can now be null (was always a string).

This affects ad preview/rendering: don’t assume a thumbnail URL is always available; fall back to other creative fields when it’s missing.

Key change: creative.thumbnailUrl: string | null

View documentation