Write
Write
POST and PATCH require the write scope. Amounts are euro cents. Send Content-Type: application/json.
https://tyfrhxfejrawgdlhryfu.supabase.co/functions/v1/advertiser-api
POST accepts Idempotency-Key. See Overview. Pause and resume take no JSON body. A non-empty body on those routes is 400.
/creatives is the same resource as /ads.
Create a campaign
POST /campaigns creates the campaign and its ad set in this organization with created_via set to api. status defaults to draft. objective defaults to traffic. budget_type defaults to daily.
name (1–120 characters) and budget_cents are required.
| Objective | Billing | Bid field |
|---|---|---|
awareness | CPM | cpm_bid_cents, €10–€100 per 1,000 impressions |
traffic, conversion | CPC | cpc_bid_cents, €0.50–€10 per click |
bid_cents is an alias of the bid for that billing model. Daily budget is €10–€1,000 (1000–100000 cents). Lifetime budget is €50–€10,000 (5000–1000000 cents). daily_budget_cents is an optional daily spend cap when budget_type is lifetime (€10–€1,000), or null to clear it.
curl -X POST 'https://tyfrhxfejrawgdlhryfu.supabase.co/functions/v1/advertiser-api/campaigns' \
-H 'Authorization: Bearer idl_adv_xxx' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: campaign-2026-11-04' \
-d '{
"name": "Spring launch",
"objective": "traffic",
"budget_type": "daily",
"budget_cents": 2000,
"cpc_bid_cents": 100,
"targeting": {
"placements": ["inline", "chat_sponsored_recommendation"],
"countries": ["FR", "DE"],
"tech_stack": { "include": ["react"], "exclude": ["vue"] }
}
}'
A successful create returns 201 and { "data": { ...campaign } }.
Other write fields:
| Field | Rule |
|---|---|
status | draft, active, or paused. active on the first API campaign is refused |
start_date, end_date | ISO-8601, or null to clear. end_date must be on or after start_date |
conversion_event | String up to 80 characters, or null |
conversion_window_days | Integer, 1–90 |
targeting.placements | At least one of the placements below |
targeting.countries | ISO country codes. [] or null means worldwide |
targeting.tech_stack | include and exclude string lists. A technology cannot be in both |
Placements: browser_overlay, browser_toast, chat_sponsored_recommendation, chat_cta_card, chat_contextual_link, chat_sponsored_answer, inline, toast, sidebar, popup.
Unknown JSON fields are 400.
Update, pause, resume
PATCH /campaigns/{id} updates only the fields you send. Targeting, bid, and budget fields update the campaign's earliest ad set.
curl -X PATCH 'https://tyfrhxfejrawgdlhryfu.supabase.co/functions/v1/advertiser-api/campaigns/CAMPAIGN_UUID' \
-H 'Authorization: Bearer idl_adv_xxx' \
-H 'Content-Type: application/json' \
-d '{ "cpc_bid_cents": 150 }'
curl -X POST 'https://tyfrhxfejrawgdlhryfu.supabase.co/functions/v1/advertiser-api/campaigns/CAMPAIGN_UUID/pause' \
-H 'Authorization: Bearer idl_adv_xxx' \
-H 'Idempotency-Key: pause-campaign-1'
Pause sets the campaign and its ad sets to paused.
curl -X POST 'https://tyfrhxfejrawgdlhryfu.supabase.co/functions/v1/advertiser-api/campaigns/CAMPAIGN_UUID/resume' \
-H 'Authorization: Bearer idl_adv_xxx' \
-H 'Idempotency-Key: resume-campaign-1'
Resume sets the campaign and its ad sets to active when the wallet has a balance and at least one ad is approved.
First campaign
The organization's first campaign cannot be turned on from this API when that campaign was created through the API (created_via is api). POST /campaigns with "status": "active", PATCH that sets status to active, and POST /campaigns/{id}/resume all return 409. The campaign stays a draft.
{
"error": {
"code": "first_campaign_requires_dashboard_validation",
"message": "This organization's first campaign was created through the API. Activate it in the adsmanager before the API can turn a campaign on.",
"campaign_id": "CAMPAIGN_UUID",
"dashboard_url": "https://adsmanager.idlen.io/campaigns/CAMPAIGN_UUID"
}
}
Open dashboard_url and turn that campaign on in Ads Manager. A later campaign is not blocked by this rule. It can go active only when the wallet has funds and at least one ad is approved. Otherwise the status is 409 and error.code is insufficient_balance, ads_pending_moderation, ad_required, or no_active_ad_set.
Create an ad
POST /ads requires campaign_id (UUID in this organization), title (1–60 characters), and cta_url (an https:// URL). body defaults to empty and is at most 150 characters. cta_text is 1–40 characters. image_url is an https:// URL or null.
curl -X POST 'https://tyfrhxfejrawgdlhryfu.supabase.co/functions/v1/advertiser-api/ads' \
-H 'Authorization: Bearer idl_adv_xxx' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: ad-2026-11-04' \
-d '{
"campaign_id": "CAMPAIGN_UUID",
"title": "Ship the integration",
"body": "Native ads in your AI product.",
"cta_text": "Get started",
"cta_url": "https://example.com/signup",
"format": "inline"
}'
The response is 201 and { "data": { ...ad } }. format and each entry of formats must be one of the placements listed above.
Moderation
A new ad is inserted with moderation applied. For a non-staff caller, is_approved stays null and is_active stays false until a person approves it. The ad does not deliver before that.
The site-creative rule can auto-approve on insert when the title, body, button, link, and image match a recent server-generated creative, the image was copied by the extractor, and the link's domain matches the organization website.
Changing the headline, body, button text, link, image, video, or media type sends the ad back to pending (is_approved null, is_active false) until it is approved again.
PATCH /ads/{id} (and PATCH /creatives/{id}) updates only the fields you send.
curl -X PATCH 'https://tyfrhxfejrawgdlhryfu.supabase.co/functions/v1/advertiser-api/ads/AD_UUID' \
-H 'Authorization: Bearer idl_adv_xxx' \
-H 'Content-Type: application/json' \
-d '{ "title": "Updated headline" }'
Ad from a URL
POST /ads/from-url uses the same landing-page extractor as Ads Manager, stores the proof, then creates the ad. Moderation still applies. This shares the extractor limit of 10 requests per organization per hour.
curl -X POST 'https://tyfrhxfejrawgdlhryfu.supabase.co/functions/v1/advertiser-api/ads/from-url' \
-H 'Authorization: Bearer idl_adv_xxx' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: ad-from-url-1' \
-d '{ "campaign_id": "CAMPAIGN_UUID", "url": "https://example.com" }'
campaign_id and url are required. url is at most 2048 characters. A page that cannot be read returns 422. Over the hourly limit returns 429 with rate_limited. POST /creatives/from-url is the same call.
Deposit Checkout
POST /wallet/checkout returns a Stripe Checkout URL. It does not charge a card. The webhook credits the wallet after payment. The session currency is EUR, matching the wallet. This response has no currency field.
amount_cents is required, from 1000 (€10) to 1000000 (€10,000), excluding tax.
success_url and cancel_url must be on https://adsmanager.idlen.io, https://ads.idlen.io, or http://localhost:3002. Anything else is replaced with the billing page.
campaign_id is optional. When that campaign is a non-archived draft in this organization, the deposit is attached to it.
The first-deposit bonus is the same rule as the billing page. The default match is €50 once: pay at least €50 excluding tax and the bonus is €50 of credit. The webhook decides the bonus from the amount paid, excluding tax and after any promotion code.
curl -X POST 'https://tyfrhxfejrawgdlhryfu.supabase.co/functions/v1/advertiser-api/wallet/checkout' \
-H 'Authorization: Bearer idl_adv_xxx' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: deposit-5000' \
-d '{ "amount_cents": 5000 }'
{
"data": {
"url": "https://checkout.stripe.com/c/pay/cs_example",
"session_id": "cs_example"
}
}
url can be null if Stripe does not return one. An organization with no owner returns 400.