Create campaigns via the API
From the client to the published (paused) ad — selection, lookups, spec, validation and submission.
1. Find the selection (Step 0)
# active clients
curl -s $BASE/clients -H "Authorization: Bearer $KEY"
# the client's Business Managers and assets (id or exact name)
curl -s $BASE/clients/<client_id>/businesses -H "Authorization: Bearer $KEY"From each Business Manager, use id as business_manager_id and one assets.ad_accounts[].id as ad_account_id. Page, Instagram, pixel and WhatsApp are optional: without them the server uses the Business Manager's defaults. Only usable ad accounts are listed; sending any other account results in 403.
"selection": { "business_manager_id": "6f1c…", "ad_account_id": "1234567890" }2. Look up what you need
| Need | Call |
|---|---|
| Interests | POST /meta/audiences/search { selection, search_type: "interests", query: "marketing" } |
| Locations | POST /meta/audiences/search { selection, search_type: "geo", query: "Goiânia", location_types: ["city"] } — Meta may also return subcities and neighborhoods; filter on type |
| Interest suggestions | POST /meta/audiences/search { selection, search_type: "interest_suggestions", interest_list: ["Medical residency"] } |
| Custom audiences | POST /meta/audiences/search { selection, search_type: "audiences" } — use only the ones with usable: true; saved audiences only with include_saved: true |
| Existing campaigns | POST /meta/campaigns/search { selection, effective_status: ["ACTIVE", "PAUSED"] } |
| Lead form | POST /meta/leadforms/search { selection } |
| Instagram posts | POST /meta/posts/search { selection, action: "search", query: "…" } |
| Targeting templates | GET /targeting-templates |
Pass the returned objects (interests, locations) through as they are — they are already in the shape the creation expects.
Custom audiences and existing campaigns are paged: 50 items per page by default (limit, up to 100). When paging.has_more is true, send paging.after back as after to get the next page.
The job_id returned by a lookup only groups that call's logs (GET /executions/{id}/logs); it is not a campaign execution.
3. Build the spec
{
"selection": { "business_manager_id": "6f1c…", "ad_account_id": "1234567890" },
"mode": "new_campaign",
"campaign": {
"name": "ACME | Traffic | Nov",
"objective": "OUTCOME_TRAFFIC",
"budget_level": "CBO",
"budget_type": "daily_budget",
"budget": 80,
"isolate_creatives": true
},
"adsets": [
{
"conversion_objective": "website_traffic",
"start_time": "2026-11-10T08:00",
"targeting": {
"age_min": 25,
"age_max": 45,
"locations": [{ "key": "2430536", "name": "São Paulo", "type": "city", "country_code": "BR" }]
}
}
],
"ads": {
"source": "drive",
"folder_url": "https://drive.google.com/drive/folders/…",
"copy": "Primary text",
"headline": "Headline",
"cta_type": "LEARN_MORE",
"link_url": "https://acme.com.br"
}
}Campaign and ad sets
objective:OUTCOME_AWARENESS,OUTCOME_TRAFFIC,OUTCOME_ENGAGEMENT,OUTCOME_LEADS,OUTCOME_SALES.budget_level:CBO(budget on the campaign,campaign.budget) orABO(budget on each ad set,adsets[].budget).conversion_objectiveandcustom_event_type(pixel event) have defaults per objective; values outside the allowed set are rejected.start_timedefaults to now;end_timeis required for a lifetime budget.targeting_template_idapplies a saved template; anytargetingfields you send override the template.- Ad set and ad names follow the web app's automatic naming unless you send
name.
isolate_creatives
isolate_creativestrue(default, except forads.source: "media_urls", where the default isfalse): send one template ad set; it is duplicated for each creative (1 creative = 1 ad set). Anameon the template becomes the base of every copy with a sequence suffix (00_SP_25-45→00_SP_25-45_01,_02…); without it, the automatic names are used.false: every creative goes into every ad set you send.
Ad source (ads.source)
ads.source)source | Fields | Notes |
|---|---|---|
drive | folder_url or feed_folder_url + stories_folder_url; copy, headline, description, cta_type, link_url, phone_number, url_tags, advantage_creative | Same rules as the folder used in the web app: a CARROSSEL/ subfolder becomes a carousel; FEED/STORIES folders turn on placement asset customization (placement_customization forces it on/off). Videos are prepared automatically. |
existing_posts | post_ids, link_url, phone_number, url_tags | Each post becomes one ad in every ad set. |
media_urls | items[] with creative_type (image/video/carousel), image_url/video_url/thumb_url/cards, texts and link_url | Does not support isolate_creatives: true (the default for this source is false). URLs must be public (https). |
UTMsEvery ad ships with Infotráfego's default
url_tags. Iflink_urlalready has UTMs or you changeurl_tags, sendconfirm_custom_url_tags: true— otherwise validation rejects it (the same confirmation the web app asks for).
Extend what already exists
{ "mode": "extend_campaign", "existing_campaign_id": "120210…", "selection": {…}, "adsets": [{…}], "ads": {…} }
{ "mode": "extend_adset", "existing_campaign_id": "120210…", "existing_adset_id": "120211…", "selection": {…}, "ads": {…} }With extend_campaign the server mirrors the optimization of the existing ad sets and continues their numbering; with extend_adset no ad set is created.
4. Validate first (recommended)
curl -s -X POST $BASE/campaigns/validate -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" -d @campaign.json{ "valid": false, "errors": { "ad_0_0_cta_type": "Botão (CTA) não disponível para o objetivo de conversão" }, "preview": null }Validation messages come back in Brazilian Portuguese, keyed by field (the example reads "Call-to-action button not available for the conversion objective").
When the spec is valid, preview shows what will be created exactly as the server resolved it (defaults, templates and names applied): campaign budget (CBO), and for each ad set its name, conversion objective and event, budget (ABO), schedule, targeting (ages, genders, locations, interests, audiences), placements, frequency cap and the ads with their CTA, link and UTMs.
5. Create the campaign
curl -s -X POST $BASE/campaigns -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" -d @campaign.json
# 202 → { "job_id": "…", "status": "queued", "status_url": "/api/v1/executions/…" }The creation runs on the server, in the background. Keep the job_id and continue with Track and retry.
Updated about 23 hours ago

