Create or update Lead Gen or Landing Page offers in Metadata platform.
Returns direct URL to access the offer in the platform.
OPERATION MODE:
- CREATE: When 'id' field is NOT included in data
- UPDATE: When 'id' field IS included in data
OFFER TYPES:
1. Lead Gen - Native forms rendered inside LinkedIn, Facebook, or Instagram. User never leaves the platform.
2. Landing Page - External page hosted on the advertiser's website. Works on EVERY channel (LinkedIn, Facebook, Instagram, Google Ads, Reddit, Microsoft Ads).
WARNING: LEAD GEN ON LINKEDIN / META CAN BE DONE TWO WAYS — pick the right one for the user's intent:
- Native Lead Gen form (offer_type='Lead Gen', data.channel=<channel>, goal=CPL): user fills the form INSIDE LinkedIn/Meta. Lower friction, higher conversion, but leads stay in the ad platform until synced. Channel-locked — see below.
- Landing Page lead capture (offer_type='Landing Page', goal=CPL, landingPageUrl + thankYouUrl + EMAIL field): user clicks through to the advertiser's own landing page and submits the form there. Choose this when the user wants branded landing pages, tracking pixels, or server-side enrichment. Channel-agnostic — one offer works on every channel.
Both flows count as Lead Gen (goal=CPL). Default to native Lead Gen for LinkedIn/Meta unless the user explicitly wants their own landing page.
WARNING: CHANNEL-LOCK ON NATIVE LEAD GEN OFFERS (read this BEFORE planning your offer batch)
The data.channel value you set on a Lead Gen offer PERMANENTLY locks the returned offer_id to that one channel. The same offer_id CANNOT be used in another channel's slot inside create_campaign / add_and_edit_campaign_elements — those tools accept the mismatch SILENTLY and produce a broken campaign that returns a successful response but cannot serve. Landing Page offers are NOT channel-locked and can be safely reused across channels.
What this means for planning:
- Want a native Lead Gen "Demo" concept on BOTH LinkedIn AND Facebook? You MUST call create_update_offer TWICE for that one concept — once with data.channel='LINKEDIN', once with data.channel='FACEBOOK'. Track both offer_ids and assign each to the matching channel slot.
- Three concepts × two native Lead Gen channels = SIX create_update_offer calls. That is correct, not over-creation. The "2-3 offers" guidance refers to distinct CONCEPTS, not call count.
- Want a single offer that works on every channel including Google? Use offer_type='Landing Page' (omit data.channel — Landing Pages have no channel field and are channel-agnostic). Google Ads ONLY accepts Landing Page offers.
- Want Lead Gen on LinkedIn using the advertiser's OWN landing page instead of LinkedIn's native form? Use offer_type='Landing Page' with goal='CPL' — then assign the returned offer_id to linkedin.offers[]. This is fully supported and a single LP offer_id can serve LinkedIn, Meta, Google simultaneously.
Right after each successful create_update_offer call, write down (offer_id, channel, concept) so you can correctly map them in the create_campaign call later. If you lose track, call get_offer(name=<offer_name>) to look up the offer and read back the channel field before submitting a campaign.
REQUIRED PARAMETERS:
- offer_type: "Lead Gen" or "Landing Page"
- data: Offer configuration object
- id: (optional) Include only when updating an existing offer
IMPORTANT LIMITS:
- offerName: MUST be unique AND under 50 characters. If name + timestamp exceeds 49 chars, shorten the base name.
WARNING: ALLOWED CHARACTERS: letters (a-z, A-Z), numbers (0-9), spaces, and these punctuation marks ONLY:
_ . ? , : ' / + - = & ; % $ # [ ] | ( )
Any other character (em-dashes, smart quotes, emoji, non-ASCII letters, ©, ™, !, @, *, {, }, <, >, ", \, ^, ~, `) will be REJECTED by the platform on Landing Page offers and stripped by this tool before the request is sent. Stick to ASCII letters/digits/space and the punctuation set above.
- offerHeader: max 60 chars
- offerDetails: max 160 chars
- thankYouMessage: max 60 chars (LinkedIn)
- privacyText: max 70 chars (Facebook/Instagram)
- Fields should always be included
CHANNEL-SPECIFIC REQUIREMENTS:
LinkedIn Lead Gen (offer_type="Lead Gen", channel="LINKEDIN"):
- REQUIRED: channel, offerName, offerHeader, thankYouMessage, thankYouUrl, privacyUrl, privacyText, fields
- CTA (linkedinCTA): REQUIRED by the backend. Defaults to DOWNLOAD_NOW when omitted. Must be one of
LEARN_MORE | VIEW_NOW | DOWNLOAD_NOW | TRY_NOW | VISIT_COMPANY_WEBSITE. Any other value silently falls
back to VISIT_COMPANY_WEBSITE on LinkedIn's side — always set linkedinCTA explicitly for predictable behavior.
- offerDetails: OPTIONAL but STRONGLY RECOMMENDED. Populates the description shown above the form on LinkedIn
and improves conversion. Not mandatory on LinkedIn (unlike FB/IG) but there's no reason to skip it.
Facebook/Instagram Lead Gen (offer_type="Lead Gen", channel="FACEBOOK" or "INSTAGRAM"):
- REQUIRED: channel, offerName, offerHeader, offerDetails, thankYouHeadline,
thankYouMessage, thankYouDescription, thankYouPageButtonText, websiteUrl, privacyUrl
- Meta's thank-you screen has THREE distinct text blocks — populate all three for a complete
UX:
• thankYouHeadline → title
• thankYouMessage → short_message (small text under the headline)
• thankYouDescription → body (longer copy). Falls back to thankYouMessage if omitted,
but supplying both is strongly preferred.
- thankYouPageButtonType (enum, default VIEW_WEBSITE). Cross-field rules:
• VIEW_WEBSITE → websiteUrl (or thankYouUrl fallback) is required
• VIEW_ON_FACEBOOK → thankYouAssetId (library creative ID) is required; without it the
backend validation will reject the offer.
- CRITICAL: offerDetails is MANDATORY when offerHeader is provided. If the user doesn't
specify offer details, generate a brief description based on the offer context.
Omitting offerDetails when offerHeader is set will cause an API validation error.
- The backend HTTP-probes followUpActionUrl (derived from websiteUrl / thankYouUrl) during
validation, so the URL MUST resolve to a live page — not a placeholder.
- linkedinCTA is ignored on FB/IG.
Landing Page (offer_type="Landing Page"):
- REQUIRED: offerName, landingPageUrl, thankYouUrl, fields
- CRITICAL: fields MUST include an EMAIL entry SPECIFICALLY. The platform's
`INVALID_LANDINGPAGE_FORM: Email mapping is required` check is satisfied
ONLY by EMAIL — BUSINESS_EMAIL alone does NOT satisfy it and the offer is
rejected. Without an email capture, the form cannot deliver a lead. If the
user does not mention form fields at all, default to:
fields: [{"name": "email", "field": "EMAIL"}, {"name": "firstName", "field": "FIRST_NAME"}, {"name": "lastName", "field": "LAST_NAME"}, {"name": "company", "field": "COMPANY"}]
BUSINESS_EMAIL may be added as an EXTRA field when the user explicitly wants
to filter out personal email domains, but it can NEVER replace EMAIL — keep
EMAIL in the form. (The server auto-corrects a lone BUSINESS_EMAIL to EMAIL.)
AVAILABLE FIELD TYPES BY OFFER TYPE:
For Lead Gen offers, the supported field types DIFFER BY CHANNEL — and the enum
SPELLINGS differ too (LinkedIn PHONE_NUMBER/ZIP_CODE vs Meta PHONE/ZIP). Mixing
them up is what causes the "Cannot deserialize Convo LeadForm JSON" attach failure.
PREFER the channel's DEFAULT-VISIBLE fields (marked [default] below) — they cover
the standard B2B form; only reach for the rest when the brief specifically needs them.
LinkedIn Lead Gen (channel="LINKEDIN") — these 22 types ONLY (source of truth:
the platform's LinkedIn Lead Gen Form dialog):
[default] "EMAIL", "WORK_EMAIL", "FIRST_NAME", "LAST_NAME", "COMPANY_NAME", "LINKEDIN_PROFILE_LINK"
[more] "PHONE_NUMBER", "WORK_PHONE_NUMBER", "COUNTRY", "CITY", "STATE", "ZIP_CODE", "JOB_TITLE",
"JOB_FUNCTION", "SENIORITY", "INDUSTRY", "COMPANY_SIZE", "DEGREE", "SCHOOL",
"FIELD_OF_STUDY", "GRADUATION_DATE", "START_DATE"
LinkedIn has NO field for REVENUE, EMPLOYEES (use COMPANY_SIZE), GENDER, DOB, MESSENGER,
STREET_ADDRESS, etc. LinkedIn caps a form at 12 fields.
Facebook/Instagram Lead Gen (channel="FACEBOOK"/"INSTAGRAM") — these 19 types ONLY
(source of truth: the platform's FB/IG Lead Gen Form dialog):
[default] "EMAIL", "WORK_EMAIL", "FIRST_NAME", "LAST_NAME", "COMPANY_NAME", "COUNTRY"
[more] "PHONE", "WORK_PHONE_NUMBER", "STREET_ADDRESS", "CITY", "STATE", "PROVINCE",
"POST_CODE", "ZIP", "GENDER", "MARITIAL_STATUS", "RELATIONSHIP_STATUS",
"MILITARY_STATUS", "JOB_TITLE"
Meta has NO field for REVENUE, EMPLOYEES, COMPANY_SIZE, SENIORITY, INDUSTRY, JOB_FUNCTION,
or the education fields (those are LinkedIn-only). "MARITIAL_STATUS" is the literal enum
(the typo is load-bearing — do not "fix" it). No field-count cap on Meta.
IMPORTANT: Do NOT use "BUSINESS_EMAIL" for Lead Gen - use "WORK_EMAIL" instead.
IMPORTANT: Do NOT use "COMPANY" for Lead Gen - use "COMPANY_NAME" instead.
IMPORTANT: Do NOT use "PHONE" for LinkedIn Lead Gen - use "PHONE_NUMBER" instead (auto-corrected if provided).
IMPORTANT: Do NOT use "PHONE_NUMBER" or "ZIP_CODE" for Facebook/Instagram - use "PHONE" / "ZIP" instead (auto-corrected if provided).
IMPORTANT: Custom / free-text questions are NOT supported. Every fields[].field MUST be one of the
predefined types for the offer's channel. Do NOT pass "CUSTOM", and do NOT invent a field for a concept
the brief names but that has no predefined type (e.g. "monthly transaction volume", "budget", "ad spend",
"use case", "team size"). Neither channel has a revenue/spend/budget field — DROP such a field. For
headcount: LinkedIn uses COMPANY_SIZE; Meta has no equivalent (drop). This server auto-normalizes near-miss
spellings (LinkedIn: EMPLOYEES->COMPANY_SIZE, ZIP->ZIP_CODE, POST_CODE->ZIP_CODE, PROVINCE->STATE; Meta:
PHONE_NUMBER->PHONE, ZIP_CODE->ZIP) and then DROPS any field the offer's channel still can't render
(logged), so the offer ships with its valid fields instead of failing at campaign-attach — but pass
channel-correct fields up front so nothing the user asked for is silently dropped.
For Landing Page offers (LandingPageFieldType):
"EMAIL", "BUSINESS_EMAIL", "FIRST_NAME", "LAST_NAME", "COUNTRY", "PHONE", "CITY", "STATE", "ZIP",
"JOB_TITLE", "SENIORITY", "COMPANY", "COMPANY_DOMAIN", "COMPANY_PHONE", "EMPLOYEES", "INDUSTRY",
"REVENUE", "LINKEDIN_URL", "UTM_SOURCE", "UTM_MEDIUM", "UTM_TERM", "UTM_CONTENT"
IMPORTANT: Do NOT use "COMPANY_NAME" for Landing Pages - use "COMPANY" instead.
IMPORTANT: Do NOT use "WORK_PHONE_NUMBER" for Landing Pages - use "PHONE" instead.
EXAMPLES:
CREATE LinkedIn Offer:
create_or_update_offer("Lead Gen", {
"channel": "LINKEDIN",
"status": "COMPLETED",
"offerName": "Free AI Marketing Audit",
"offerHeader": "Unlock Growth with AI",
"offerDetails": "A 30-minute audit of your current AI stack with tailored recommendations.",
"linkedinCTA": "DOWNLOAD_NOW",
"thankYouMessage": "We'll contact you soon!",
"thankYouUrl": "https://example.com/thanks",
"privacyUrl": "https://example.com/privacy",
"privacyText": "We respect your privacy",
"fields": [
{"name": "email", "field": "EMAIL"},
{"name": "firstName", "field": "FIRST_NAME"},
]
})
UPDATE LinkedIn Offer:
create_or_update_offer("Lead Gen", {
"id": 62148,
"channel": "LINKEDIN",
"status": "COMPLETED",
"offerName": "Free AI Marketing Audit 2025",
"offerHeader": "Unlock Growth with AI",
"offerDetails": "A 30-minute audit of your current AI stack with tailored recommendations.",
"linkedinCTA": "LEARN_MORE",
"thankYouMessage": "We'll contact you soon!",
"thankYouUrl": "https://example.com/thanks",
"privacyUrl": "https://example.com/privacy",
"privacyText": "We respect your privacy",
"fields": [
{"name": "email", "field": "EMAIL"},
{"name": "firstName", "field": "FIRST_NAME"},
]
})
CREATE Facebook/Instagram Offer:
create_or_update_offer("Lead Gen", {
"channel": "FACEBOOK",
"status": "COMPLETED",
"offerName": "Free Consultation",
"offerHeader": "Get Expert Advice",
"offerDetails": "Book a 30-minute consultation with our senior strategists.",
"thankYouHeadline": "Thank You!",
"thankYouMessage": "We'll be in touch soon",
"thankYouDescription": "A member of our team will email you within one business day to schedule your session.",
"thankYouPageButtonText": "Visit Website",
"thankYouPageButtonType": "VIEW_WEBSITE",
"websiteUrl": "https://example.com",
"privacyUrl": "https://example.com/privacy",
"privacyText": "Privacy Policy",
"fields": [
{"name": "email", "field": "EMAIL"},
{"name": "firstName", "field": "FIRST_NAME"},
]
})
CREATE Landing Page Offer:
create_or_update_offer("Landing Page", {
"offerName": "Free Demo",
"landingPageUrl": "https://example.com/landing",
"thankYouUrl": "https://example.com/thanks",
"fields": [
{"name": "email", "field": "EMAIL"},
{"name": "firstName", "field": "FIRST_NAME"},
]
})
RESPONSE: Success returns offer_id, offer_url, and configuration details.
VALIDATION: Checks URLs start with https://, character limits, required fields per channel.