Messaging Ads

Create messaging ad

Changed on

Creates a Meta ad that opens a chat with you on WhatsApp, Messenger or Instagram Direct when someone taps it.

Creates a click-to-message ad; destination selects where the tapped ad opens a conversation: WhatsApp, the Page's Messenger inbox or the linked Instagram account's Direct inbox. destinations puts two or three of them on one ad set and lets Meta pick the app per viewer. The ad set is created with the matching destination_type and CONVERSATIONS optimization; the campaign objective defaults to OUTCOME_ENGAGEMENT. Supports single-creative and multi-creative shapes. Supersedes POST /v1/ads/ctwa (deprecated, equivalent to destination: whatsapp). Existing posts and reels are supported through platformPostId (alias existingPostId) or objectStoryId, either per creative or at the top level. Omit fresh media and copy for that creative. Optional whatsappPhoneNumber selects a number already paired with the Page (WhatsApp destination only). accountId is a Facebook, Instagram or Meta ads (business login) connection; pageId picks the Page when that connection was granted several.

Required fields per shape (a missing one is a 400 naming it in param):

  • Always accountId, adAccountId, name, and exactly one of destination / destinations.
  • budgetAmount and budgetType unless adSetId or existingCampaignId is set, plus endDate for a lifetime budget.
  • Single image or video: headline, body, and exactly one of imageUrl / video. With placementAssets, headline and body only (the media comes from placementAssets).
  • carouselCards: body and the 2-10 cards; headline is optional, imageUrl / video / top-level description are a 400.
  • Existing post (platformPostId or objectStoryId): no copy or media fields.
  • creatives[]: the same rules per entry, with no top-level creative fields.
  • welcomeMessage, when sent: text plus exactly one of prefillText / quickReplies.

WhatsApp number prerequisite. For the WhatsApp destination, link the WhatsApp number to the Facebook Page used for the ad before creating it: Meta checks the pairing at creation. Link it from the WhatsApp Business app (Settings > Business tools > Facebook & Instagram) or from the Page settings (Linked accounts > WhatsApp). Linking does not affect the number's WhatsApp connection in Zernio (Cloud API or coexistence), so no reconnect or re-sync is needed. If Meta creates a new ad set without the requested whatsappPhoneNumber, the 201 response carries a warnings entry. This can happen even when the number is linked (Meta can drop a Page's second linked number).

Idempotency: this endpoint is not idempotent at the platform level (a blind retry creates a second campaign/ad set/ad). Send an Idempotency-Key header to make retries safe: the first request with a given key creates the ad and we store the response; a retry with the same key replays that exact response (with Idempotent-Replayed: true) instead of creating duplicates. Reusing a key with a different body returns 422; a key whose first request is still in flight returns 409 (retry after a short backoff). If the first request is cut off mid-create (for example by a function timeout), its key can stay in progress and keep returning 409 until it expires; use a new key to retry sooner, after checking whether the ad was created. Keys are scoped to your credential and expire after 24h.

post/v1/ads/messaging

Request

  • Base URL: https://zernio.com/api
  • URL: https://zernio.com/api/v1/ads/messaging
  • Auth: HTTP bearer

Headers

Idempotency-Keystring

Optional client-generated unique key (e.g. a UUID) that makes retries safe. Same key + same body replays the original response; same key + different body → 422; key still processing → 409. A request cut off mid-handler (for example by a function timeout) can leave its key in progress, answering 409 until the key expires after 24 hours.

Request body

creativeFeaturesMetaCreativeFeatures

Meta Advantage+ creative enhancements. Map snake_case feature names to OPT_IN or OPT_OUT; Meta validates supported keys and unspecified features default to OPT_OUT. auto_promotion_tag is an Advantage+ enhancement, not the Ads Manager Promotion setting. The deprecated standard_enhancements bundle is rejected by Meta.

accountIdstring required

Facebook or Instagram SocialAccount ID.

adAccountIdstring required

Meta ad account ID, e.g. act_123456789.

namestring required

Ad display name. Used to derive campaign / ad set names. On the multi-creative shape, each ad's Meta name gets a " #N" suffix (1-indexed) so Ads Manager shows them as a numbered batch.

campaignNamestring

Exact name for the campaign this request provisions. Omitted keeps <name> - Campaign. Ignored with adSetId (the ad set already has a campaign).

adSetNamestring

Exact name for the ad set this request provisions. Omitted keeps <name> - Ad Set. Ignored with adSetId.

platformPostIdstring

Messaging and CTWA only. Platform post or reel ID, the same input boostPost takes as platformPostId. Facebook IDs become object_story_id; Instagram IDs become source_instagram_media_id run as the media owner (resolved from the media on a Meta ads business-login connection, so no Instagram connection is needed). Mutually exclusive with objectStoryId and fresh creative fields.

existingPostIdstring

Alias of platformPostId, kept for existing callers. Sending both with different values is a 400.

objectStoryIdstring

Messaging and CTWA only. Raw Facebook pageId_postId reference, used as object_story_id even with an Instagram account. Mutually exclusive with platformPostId and fresh creative fields.

pageIdstring

Facebook Page the ad runs as, when the connection was granted several Pages. Defaults to the Page bound to the connection. Any Page granted to the connection is accepted; other ids answer 400 listing the granted Pages. Same semantics as pageId on POST /v1/ads/create.

whatsappPhoneNumberstring

WhatsApp only. Optional E.164 number already paired with the Facebook Page. Omit to let Meta select the paired number. Sent to the creative CTA and, when creating a new ad set, its promoted_object. Meta can accept the number yet create the ad set without it; the response then carries a warnings entry. Attach requests do not change the existing ad set. Stored as creative.whatsappPhoneNumber on every created ad.

headlinestring

Single-creative shape only. Mutually exclusive with creatives[]. Required on a single image or video ad (including one built with placementAssets); not used by a carouselCards ad and not accepted with an existing post reference.

bodystring

Primary text shown above the image / video. Single-creative shape only. Mutually exclusive with creatives[].

descriptionstring

Link description, independent of headline and body (Meta's link_data.description, video_data.link_description on video, and the shared description of a placementAssets feed). Meta shows it mainly on Facebook Feed placements, under the headline, when there is room; Instagram, Stories, Reels and Messenger placements do not display it. Also accepted per entry in creatives[]. Not allowed with an existing post creative.

imageUrlstring uri

Image asset for single-creative shape. Mutually exclusive with video and with creatives[]. Required on the single-creative shape if neither video nor an existing post reference is supplied.

optimizeCardOrderboolean

Only with carouselCards (a 400 without them). Sets the creative's link_data.multi_share_optimized. false keeps the cards in the order given; Meta then takes at most 5 cards, so false with more than 5 carouselCards is a 400. true lets Meta pick and reorder the cards for performance (up to 10). Omitted, nothing is sent and Meta's default (true) applies. Single-creative shape; for creatives[] set it per entry.

adSetIdstring

Attach the creatives to this EXISTING messaging ad set instead of building a campaign, so the ad set keeps its learning phase. It then owns budget, targeting and schedule, so budgetAmount, budgetType, budgetLevel, startDate, endDate, objective, campaignStatus, adSetStatus, existingCampaignId, the special ad category fields and every targeting field except ageMin, ageMax, placements and advantageAudience are rejected with a 400 alongside it. Its destination_type must match the ad's destination.

existingCampaignIdstring

Create the new messaging ad set (and its ads) under this EXISTING Meta campaign instead of a new one, e.g. several audience ad sets under one campaign. The campaign's objective must be OUTCOME_ENGAGEMENT, OUTCOME_SALES or OUTCOME_LEADS (400 otherwise). If the campaign has a campaign budget, omit budgetAmount and budgetType (400 if sent); otherwise they are required and land on the new ad set. objective, campaignName, campaignStatus, budgetLevel, specialAdCategories, specialAdCategoryCountry and adSetId are rejected alongside it. To add ads to an existing ad set instead, use adSetId.

budgetLevel'adset' | 'campaign'

Where the budget lives. adset (default) puts it on the new ad set. campaign creates an Advantage campaign budget (CBO): the budget and bid strategy sit on the campaign and the ad set inherits them, same as POST /v1/ads/create. Not allowed with adSetId or existingCampaignId.

budgetAmountnumber

Budget amount in the ad account's currency major units (e.g. dollars for USD, not cents). Must be > 0. Required unless adSetId is set (the ad set owns it) or existingCampaignId names a campaign with a campaign budget.

budgetType'daily' | 'lifetime'

Required unless adSetId is set or existingCampaignId names a campaign with a campaign budget. lifetime requires endDate.

currencystring

ISO 4217 currency code matching the ad account's currency (e.g. USD). Optional: Zernio resolves it from the ad account when omitted. The value selects the minor-unit exponent Zernio converts budget/bid amounts by before calling Meta (most currencies are cents; zero-decimal currencies like JPY/KRW are sent as-is).

startDatestring

When the ad set starts delivering. ISO 8601 date or date-time. A value with an offset (2027-01-15T10:00:00+01:00, ...Z) is used as is; one without an offset (2027-01-15T10:00:00) is read in the ad account's timezone, and a date-only value starts at 00:00 local. Defaults to now.

endDatestring

ISO 8601 date or date-time, read like startDate; a date-only value ends at 23:59:59 local. Required when budgetType is lifetime.

countriesstring[]

ISO 3166-1 alpha-2 country codes. Defaults to ["US"] only when no other geo (cities, regions, zips, metros, customLocations) is supplied.

countryGroupsstring[]

Meta only. Continents and trade blocs (geo_locations.country_groups), for targeting a whole region without listing its countries. Combines with countries rather than replacing it, and is also accepted under excludedLocations. Discoverable via GET /v1/ads/targeting/search?dimension=geo&geoType=country_group.

ageMininteger
ageMaxinteger
audienceIdstring

Custom audience ID to target.

gender'all' | 'male' | 'female'

Restrict the audience by gender (Meta genders). Omit or send all for everyone; all is ignored in adSetId attach mode. Stored on the ad and read back in targeting.gender.

languagesstring[]

Audience languages (Meta locales). A bare ISO 639-1 code targets all regional variants ("en" = all English), a region-qualified code a specific one ("en_GB", "pt_BR"); unknown codes are rejected.

excludedLocationsobject

Geo to exclude, same shape as POST /v1/ads/create (countries, countryGroups, regions, cities, zips, places, neighborhoods, customLocations).

incomeTier'top_5' | 'top_10' | 'top_10_25' | 'top_25_50'

Normalized household-income tier, same as POST /v1/ads/create. Incompatible with housing, employment and credit specialAdCategories.

userOsstring[]

Meta user_os, e.g. ["iOS_ver_14.0_and_above"].

userDevicestring[]

Meta user_device.

audienceIncludestring[]

Custom or lookalike audience ids to include.

audienceExcludestring[]

Custom or lookalike audience ids to exclude.

savedTargetingIdstring

ID of a saved_targeting audience (POST /v1/ads/audiences), expanded as the base targeting. Precedence: savedTargetingId, then targeting, then the flat fields.

rawTargetingobject

Meta targeting spec sent as the BASE layer of the ad set's targeting, exactly as POST /v1/ads/create does: use it for anything the flat fields cannot express, such as a layered flexible_spec (entries AND together, ids inside one entry OR). Flat fields you also send are layered on top and win per key. With rawTargeting present the US geo and advantage_audience: 0 defaults are not injected, so include targeting_automation in it (or send advantageAudience), as Meta requires it on create.

specialAdCategoriesstring[]

Meta special ad categories on the new campaign.

specialAdCategoryCountrystring[]

Countries the special ad category applies to. Requires specialAdCategories.

advantageAudience0 | 1

Meta's Advantage+ audience expansion. 0 (default) keeps targeting strict; 1 lets Meta expand beyond the supplied targeting when its delivery system finds better matches. Always sent on CREATE (Meta requires it).

objective'OUTCOME_ENGAGEMENT' | 'OUTCOME_SALES' | 'OUTCOME_LEADS'

Defaults to OUTCOME_ENGAGEMENT. OUTCOME_SALES and OUTCOME_LEADS require additional account configuration (Dataset linked to the WABA for sales) and may be rejected by Meta if missing.

status'ACTIVE' | 'PAUSED'

Defaults to ACTIVE. PAUSED pauses only the top-most object this call creates: the new campaign (ad set and ads switched on), or, with adSetId, the new ads themselves.

campaignStatus'ACTIVE' | 'PAUSED'

Campaign-level status, same semantics as POST /v1/ads/create. Defaults to status. PAUSED holds the new campaign off while the ad set and ads switch on (one resume call brings the whole hierarchy live); ACTIVE with status: PAUSED switches the campaign on and pauses the new ad set instead. Only meaningful when a new campaign is being created; rejected with a 400 alongside adSetId (the attach shape reuses an existing campaign).

adSetStatus'ACTIVE' | 'PAUSED'

Ad-set-level status, same semantics as POST /v1/ads/create. Sets the new ad set's switch alone and overrides status for it. A level status (campaignStatus, adSetStatus, adStatus) always wins for its level, and status: PAUSED adds a hold of its own only when none of them is PAUSED. Rejected with a 400 alongside adSetId (that ad set already exists).

adStatus'ACTIVE' | 'PAUSED'

Request-side publish switch for adStatus on ad creation endpoints only (status, campaignStatus and adSetStatus carry their own inline ACTIVE/PAUSED enum there; the status-update endpoints are a separate lowercase active/paused enum). Distinct from AdStatus (the platform-reported delivery status returned on reads).

bidStrategy'LOWEST_COST_WITHOUT_CAP' | 'LOWEST_COST_WITH_BID_CAP' | 'COST_CAP' | 'LOWEST_COST_WITH_MIN_ROAS'

Meta bid strategy applied to the shared ad set. Defaults to LOWEST_COST_WITHOUT_CAP (auto-bid) when omitted. LOWEST_COST_WITH_BID_CAP and COST_CAP require bidAmount. LOWEST_COST_WITH_MIN_ROAS requires roasAverageFloor. CTWA's optimization_goal is fixed to CONVERSATIONS, but the bid strategy is independent.

bidAmountnumber

Whole currency units (e.g. 5 = $5.00 on a USD account). Required when bidStrategy is LOWEST_COST_WITH_BID_CAP or COST_CAP; rejected otherwise.

roasAverageFloornumber

Decimal ROAS multiplier (e.g. 2.0 = 2.0× ROAS floor). Required when bidStrategy is LOWEST_COST_WITH_MIN_ROAS; rejected otherwise. Meta enforces its own upper bound server-side.

dsaBeneficiarystring

Legal entity that benefits from the ad. Required when targeting EU users (EU DSA, Article 26). Optional if the ad account has a default beneficiary: set it once via PATCH /v1/ads/accounts or in Meta Ads Manager, and Meta fills it in whenever the field is omitted.

dsaPayorstring

Legal entity that pays for the ad. Can differ from dsaBeneficiary (for example, an agency paying for a client's ads). Same rules as dsaBeneficiary: required for EU targeting unless the ad account has a default payor.

regionalRegulatedCategoriesstring[]

Meta only. Regional regulation categories required when the ad set targets certain countries (e.g. AUSTRALIA_FINSERV, AUSTRALIA_UNIVERSAL, BRAZIL_REGULATION, INDIA_FINSERV, MALAYSIA_UNIVERSAL, SINGAPORE_UNIVERSAL, TAIWAN_FINSERV, TAIWAN_UNIVERSAL, THAILAND_UNIVERSAL). Forwarded to the ad set.

regionalRegulationIdentitiesobject

Meta only. Verified identity IDs of the beneficiary and payer for each declared regionalRegulatedCategories value. Meta may require them on ad sets created through the API even when a default beneficiary and payer are set in Ads Manager advertising settings (seen on AUSTRALIA_FINSERV), so sending them explicitly is the reliable path. The value is the Verified identity ID (Meta Business Settings > Verified identities), not a Page or Business ID, and beneficiary and payer may be the same ID. Keys by category: AUSTRALIA_FINSERV uses australia_finserv_beneficiary / australia_finserv_payer; TAIWAN_FINSERV uses taiwan_finserv_beneficiary / taiwan_finserv_payer; INDIA_FINSERV uses india_finserv_beneficiary / india_finserv_payer; TAIWAN_UNIVERSAL uses taiwan_universal_beneficiary / taiwan_universal_payer; SINGAPORE_UNIVERSAL uses singapore_universal_beneficiary / singapore_universal_payer; AUSTRALIA_UNIVERSAL, BRAZIL_REGULATION, MALAYSIA_UNIVERSAL and THAILAND_UNIVERSAL use universal_beneficiary / universal_payer. When Meta rejects the ad set for a missing verified advertiser, the error carries param regionalRegulationIdentities, details.expectedIdentityKeys (the keys the declared categories expect), details.suggestedCategories (the markets Meta ties to that subcode) and Meta's metaCode, metaSubcode and fbtraceId. Ignored when adSetId adds the ad to an existing ad set.

destination'whatsapp' | 'messenger' | 'instagram_direct'

Where the conversation opens when the ad is tapped. Set this OR destinations, not both.

destinationsstring[]

Two or three messaging apps on ONE ad set, like Ads Manager's "all messaging apps": the ad set gets Meta's combined destination_type (e.g. MESSAGING_INSTAGRAM_DIRECT_MESSENGER_WHATSAPP) and the creative one CTA per app, so Meta opens the app each viewer is likeliest to answer from. WhatsApp in the list still needs the Page paired with a WhatsApp Business number. With adSetId, the existing ad set must already use that combined destination_type. Set this OR destination, not both.

workflowIdstring

A workflow in the ad account's profile. When someone clicks the ad, that workflow starts in the conversation the click opens (on the first message, a postback or a standalone referral carrying the ad id), ahead of keyword-matched workflows, if it is active on the receiving account. Stored on the ad. 404 when no such workflow exists in the profile.

validateOnlyboolean

Dry-runs the ad on Meta with execution_options validate_only as ONE inline campaign + ad set + creative + ad (or creative + ad on the existing ad set with adSetId). Nothing is uploaded or created and nothing is stored; media is checked by URL. Supports one creative with imageUrl, image placementAssets, carouselCards, an existing video.id, or an existing post. Several creatives, a new video.url and video placementAssets need uploads first and return 400. Success returns 200 with per-node results; a Meta rejection returns the Meta error.

Example request

{
  "creativeFeatures": {
    "auto_promotion_tag": "OPT_IN"
  },
  "creatives": [
    {
      "creativeFeatures": {
        "auto_promotion_tag": "OPT_IN"
      }
    }
  ],
  "placementAssets": {
    "defaultImageUrl": "https://example.com/feed-4x5.jpg",
    "rules": [
      {
        "imageUrl": "https://example.com/feed-4x5.jpg",
        "headline": "Build with Zernio",
        "body": "Schedule social posts from your app.",
        "description": "One API for your social content.",
        "placements": {
          "publisherPlatforms": [
            "facebook"
          ],
          "facebookPositions": [
            "feed"
          ]
        }
      },
      {
        "imageUrl": "https://example.com/story-9x16.jpg",
        "headline": "Your next story",
        "body": "Bring your social workflow to Zernio.",
        "placements": {
          "publisherPlatforms": [
            "facebook"
          ],
          "facebookPositions": [
            "story",
            "facebook_reels"
          ]
        }
      }
    ]
  }
}

Response

validateOnly: true only. Meta accepted every node; nothing was created.

validateOnlyboolean required

Always true.

messagestring required

Changes

    • info

      added the new optional request property /////

    • warning

      added the new unverified enum value to the // response property for the response status