Zernio
APIFor you
Zernio
APIFor you
Platforms

Meta Ads

Overview

Set up

Connect

Build

CampaignsAd setsCreativesCreative libraryPreviewsBoost a postLead Gen Forms

Target

TargetingCustom Audiences

Ad types

Creative testingConversion campaignsCatalog adsMessaging & call adsClick-to-WhatsApp adsReach & frequency

Measure

InsightsMeta PixelsConversions APIAd URL tracking tags

Operate

Duplicate & lifecycleAccount & ops readsAd LibraryAd comments

Reference

Enums, limits & error codes
Dashboard
OverviewPlatformsAPI ReferenceResources
Meta Ads

Lead Gen Forms

Create a Meta instant form with POST /v1/ads/lead-forms, attach it to an ad, and receive the leads by webhook or by polling.


Collect leads inside Meta instead of on your site:

  1. Create an instant form.
  2. Attach it to an ad with goal: "lead_generation".
  3. Receive submissions on the lead.received webhook.

Forms are Page-scoped. Use a connected Facebook account, or a metaads business-login connection with a selected Page, as accountId.

Instagram lead ads use the Page's form.

The Page must accept Facebook's Lead Generation terms once, at https://www.facebook.com/ads/leadgen/tos/, before lead ads can run. Until it does, ad creation returns Meta subcode 1815089. Creating forms and reading leads work without it.

Create a form

Call POST /v1/ads/lead-forms with accountId, name, privacyPolicyUrl and the form content in platformSpecificData.

Prefilled question types (EMAIL, PHONE, FULL_NAME, FIRST_NAME, LAST_NAME) generate their own label and key. Send only { "type": "..." }.

A CUSTOM question needs key, label and, for a choice question, options.

import Zernio from '@zernio/node';

const zernio = new Zernio();

const { data: created } = await zernio.leadgen.createLeadForm({
  body: {
    accountId: '66b2e19d8c3f5a7e9d0b1c2d',
    name: 'Spring promo',
    privacyPolicyUrl: 'https://example.com/privacy',
    platformSpecificData: {
      questions: [
        { type: 'EMAIL' },
        { type: 'FULL_NAME' },
        {
          type: 'CUSTOM',
          key: 'budget',
          label: 'Monthly budget?',
          options: [
            { key: 'low', value: 'Under $1k' },
            { key: 'high', value: '$1k and above' }
          ]
        }
      ]
    }
  }
});

const formId = created.form.id;

Response (200):

{
  "status": "success",
  "form": { "id": "1029384756102938", "name": "Spring promo" }
}

platformSpecificData also carries:

  • the thank-you screen (thankYouTitle, thankYouBody, thankYouButtonText, thankYouButtonType, thankYouWebsiteUrl)
  • the contextCard
  • locale
  • followUpActionUrl
  • isOptimizedForQuality, which adds a review step before submit

It is strict-parsed: an unknown key returns a 400.

The same endpoint serves LinkedIn Ads with a LinkedIn-shaped platformSpecificData, selected by accountId.

The older top-level Meta fields still work while platformSpecificData is absent. Mixing the two shapes in one request is a 400.

Creates are not idempotent, so never auto-retry one.

Cover photo. Set platformSpecificData.contextCard.coverPhoto to a direct public JPEG or PNG URL, up to 5 MB.

Zernio uploads it as an unpublished Page photo and attaches it to the form. Redirects, Ad Image hashes and image IDs are unsupported.

Attach the form to an ad

Send goal: "lead_generation" and leadGenFormId on the create request. This also works with carousel creatives.

No linkUrl is needed: the form is the destination. The ad set's lead optimization and promoted Page are derived for you.

{
  "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
  "adAccountId": "act_1234567890",
  "name": "Spring promo lead ad",
  "goal": "lead_generation",
  "leadGenFormId": "1029384756102938",
  "budgetAmount": 10,
  "budgetType": "daily",
  "headline": "Get a quote",
  "body": "Tell us about your project",
  "callToAction": "SIGN_UP",
  "imageUrl": "https://cdn.example.com/creative.jpg"
}

POST /v1/ads/boost takes the same 2 fields to put a form behind an existing post.

Every ad attached to a lead ad set needs its own leadGenFormId (creatives).

Receive leads

The lead.received webhook fires the moment a lead is submitted.

Its payload carries lead.fields (the question key to answer map) plus the formId and adId provenance.

Poll GET /v1/ads/lead-forms/{formId}/leads when you would rather pull.

const { data: page } = await zernio.leadgen.listFormLeads({
  path: { formId: '1029384756102938' },
  query: { accountId: '66b2e19d8c3f5a7e9d0b1c2d', limit: 50 }
});

for (const lead of page.leads) console.log(lead.fields);

Response (200):

{
  "status": "success",
  "leads": [
    {
      "id": "5566778899001122",
      "createdTime": "2027-02-14T10:31:05+0000",
      "adId": "120260000000000000",
      "formId": "1029384756102938",
      "fields": { "email": "lead@example.com", "full_name": "Jamie Rivera", "budget": "high" }
    }
  ],
  "pagination": { "hasMore": false, "cursor": null }
}

GET /v1/ads/leads is the cross-form view of the same data: every lead the connected account has captured, filterable by formId and since.

It is the shape to feed a CRM without iterating forms.

since is Unix seconds, between 1 and 253402300799. Millisecond timestamps return 400 with instructions to divide by 1,000.

Test a lead before you spend

POST /v1/ads/lead-forms/{formId}/test-leads puts a synthetic lead on the form. Use it to exercise your webhook handler and field mapping first.

It takes accountId and fieldData, an array of at least one { name, values[] } entry matching the form's questions.

curl -X POST "https://zernio.com/api/v1/ads/lead-forms/1029384756102938/test-leads" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "fieldData": [{ "name": "email", "values": ["test@example.com"] }]
  }'

Response (200):

{ "status": "success", "testLead": { "id": "5566778899001123" } }

Meta allows one pending test lead per form, so consume or delete it before creating another.

To retire a form, call DELETE /v1/ads/lead-forms/{formId}. Meta archives it, and there is no hard delete.

If it fails

A 422 on the form create is Meta rejecting the form:

{
  "error": "Meta rejected the lead form",
  "type": "platform_error",
  "platform": "meta",
  "platformError": { "code": 3 }
}

Code 3 is Meta's generic app-capability error and does not name a field.

When the request set isPhoneSmsVerifyEnabled, the response names that field as the one to drop first. The toggle then has to be set in Meta's form builder.

On the ad create, subcode 1815089 is the Lead Generation terms the Page has not accepted yet.

Related

  • Campaigns: the create request leadGenFormId goes on.
  • Conversion campaigns: how lead_generation differs from lead_conversion.
  • Ads webhooks: the lead.received event and its payload.
  • Create lead form and List form leads: every field.
Was this page helpful?

Boost a post

Turn a published Facebook or Instagram post into a Meta ad with POST /v1/ads/boost, keeping the post's likes and comments.

Targeting

Target Meta ads by country, city, region, ZIP, age, gender and interests, and resolve Meta's opaque location and interest ids with GET /v1/ads/targeting/search.

On this page

Create a formAttach the form to an adReceive leadsTest a lead before you spendIf it failsRelated