API Reference
Changelog
Every developer-facing change to the /api/v1 surface, newest first
Developer-facing changes to https://api.atribu.app/api/v1/**. See the deprecation policy for what a removal looks like before it happens.
Active deprecations
| Operation | Deprecated | Sunset | Successor |
|---|---|---|---|
POST /api/v1/goals | 2026-09-05 | 2026-12-04 | /api/v1/goals/definitions |
Changes
Each line links the issue that carries the full reasoning — what broke, what to change, and why the decision went the way it did.
2026-09-24
- Added · #1766 — Journey reads for keys without
customers:read.GET /api/v1/conversions/{id}/journeynow opens atconversions:read: a key withoutcustomers:readgets the key-only projection (touch, channel, campaign / ad set / ad by platform id, click-id kind, join method, credit,in_window) withmeta.conversion.customer_key— your own customer id — and no person, page, device, geo or session fields at all; a key with it gets the same response as before. New:GET /api/v1/customers/journey?customer_key=returns one person's whole path by theexternal_idyou ingest, withmeta.person(counts and the newest 50 conversions);?email=/?phone=needcustomers:read. Healthcare-family profiles' keys now also leave outvisitors:read(policy S5). - Added · #1696 — Apply Atribu's URL parameters to Meta ads.
POST /api/v1/quality/ad-ids/url-tags/applywrites them onto up to 25 of the adsGET /api/v1/quality/ad-idslists, merged with each ad's own, and returns one result per ad (applied, skipped with a reason, or Meta's refusal). Every call must carryacknowledge_learning_reset: true: the change replaces the ad's creative (same post), which Meta treats as a significant edit.…/undoputs one ad back on its previous creative;GET …/url-tagssays whether the profile can apply at all (ads_management+campaigns:apply) and lists recent changes. - Added · #1676 — Partner ad operations.
POST /api/v1/ads/{id}/pauseand/resumepause or resume one ad by its Meta id with no recommendation behind it, andPOST /api/v1/adsets/{id}/budgetchanges one ad set's daily budget (±50 % of the live budget per call,422 budget_change_out_of_boundsoutside it) — allcampaigns:apply,Idempotency-Keyrequired, audited and reversible. A connection whose Meta grant lacksads_managementanswers403 meta_write_permission_missingbefore anything is written.GET /api/v1/conversions/{id}/journeyreturns the visitor timeline behind one conversion, addressed by itsoutcome_event_id,conversions.idor your own event key. - Changed · #1676 — On a key minted by the app that provisioned the profile,
applied_bymay be omitted: the apply is attributed to the workspace's owner (the user provisioning created) while it is still the active owner. An explicitapplied_by, and every other key, behave as before.
2026-09-23
- Added · #1670 —
POST /api/v1/eventsaccepts an optionalaction_sourcein Meta's vocabulary (website,business_messaging,physical_store,system_generated,app,phone_call,chat,email,other). It reaches Meta verbatim on every Conversions API export of the event, in every privacy mode, and any value other thanwebsiteships with noevent_source_url— even when the buyer has fbc/fbp browser signals. Abusiness_messagingevent withclick_ids.ctwa_clidgoes to the dataset linked to the profile's WhatsApp Business Account; without that dataset or a click id it is sent aschat. Omitting the field keeps the previous inference. - Added · #1670 — Each deliveries ledger entry now carries
action_source: the Metaaction_sourcethe stored payload carried (the declared value when there was one, else Atribu's inference).nullfor a non-Meta delivery or a row skipped before a payload was built. - Changed · #1669 — Platform-Safe now sends to the profile's EXISTING Meta dataset by default. A new
platform_safe_target(existing|clean, read on the catalog settings and onGET /api/v1/exports/destinations) decides which dataset gets the scrubbed feed: underexistingthe ordinary Meta CAPI destinations ship and nothing is recreated; undercleanonly the Clean Dataset ships, as before. Profiles that already had a Clean Dataset were moved toclean, so their routing is unchanged. A successful provision sets the target toclean, and itschecksreport it. Deliveries held for a leftover Clean Dataset underexistingare skipped withplatform_safe_clean_dataset_inactive. - Added · #1669 —
POST /api/v1/conversion-sync/platform-safe/targetswitches a Platform-Safe profile between its existing dataset and a Clean Dataset.cleanprovisions the Clean Dataset and lists the ad sets to recreate (the target flips only when a Clean Dataset exists); going back toexistinganswers409 clean_dataset_has_live_ad_setswhile Clean Dataset ad sets still deliver, unlessconfirm_live_ad_sets: true. The wiring read'splatform_safeblock gainstarget,target_destination_ids,event_mapping(the event name each rule sends, with a 7-day double-count check against the dataset's own counts),categorization(the Events Manager checklist and the openplatform_safe_events_not_acceptedalert) and theswitch_to_clean_datasetremediation; underexistingit has no provision or recreate step. Clean Dataset ad sets now optimize on the dataset's standard event (optimize_on: standard_event), not on a custom conversion that Core Setup would silence. - Added · #1668 — A partner app's DPA is accepted once, at the app. An owner or admin of the workspace that administers the app accepts it in the console (Workspace settings → Compliance), and every profile the app provisioned — and every one it provisions later — reads
dpa_status: acceptedwith that version and acceptor, sosign_dpano longer appears for them. Withdrawing returns those inherited profiles towithdrawn; a profile its own owner accepted keeps its acceptance. An API key or MCP token still cannot accept. - Fixed · #1653 — An email send the mailbox's provider refuses is no longer a
502. A refusal about the mailbox answers its own code —403 account_suspended,403 mailbox_disabled,403 auth_revoked(withreconnect_required: true) or429 quota_exceeded— a refused recipient answers422 recipient_rejected, and every one carries the provider's own words inprovider,provider_status,provider_codeandprovider_message. The connection reportsstatus: "needs_action"(orreconnect_requiredfor a dead grant) with the provider's message instatus_reason, and achannel.health.updatedevent is pushed on provideremail; the first send the provider accepts clears it. Only a transient provider failure still answers502/503. - Fixed · #1653 — Inbound email
delivery_failurenow carries the report'sstatuson Gmail bounces — it was alwaysnullthere, because Gmail nests the per-recipient fields one part deeper than Atribu read, so a permanent5.x.xlooked like a soft bounce. It also gainsaction(failed,delayed,delivered,relayedorexpanded) anddiagnostic_code(the remote server's own words), bothnullwhen the report has none. - Fixed · #1653 — An email send from an Outlook mailbox now answers a real
provider_message_id— the Microsoft Graph immutable id of the sent message (its copy in Sent Items) — instead of"", for a new thread and for a reply alike.thread_idandrfc822_message_idare unchanged. Inbound Outlook events keep Graph's default-format id, so recognise your own send byrfc822_message_id, not by comparing ids. - Changed · #306 — The automated duplicate-and-swap is open to partners behind a consent contract. An API key or MCP user token that sends
confirm_recreate: trueorconfirm_duplicate_swap: truemust also sendconsent: { version, text_hash, accepted_by, accepted_at }for the current consent text (2026-09-23.duplicate-swap.v1, published on the Partners page); without it the call is409 consent_requiredand nothing reaches Meta. Signed-in sessions are unchanged. Both responses gainwarnings(new ad set ids, learning reset, double spend), and error envelopes may carryconsent. - Changed · #306 — Platform-Safe provisioning is ready for partners. An API key or MCP user token must switch the profile to
platform_safefirst, or gets409 platform_safe_required; no usable Meta Ads connection is409 meta_connection_required. A Clean Dataset that already covers every tracked outcome answersstatus: "already_provisioned"with no Meta call, and every 200 now carrieschecks(privacy mode, Meta connection,ads_management, mapped outcome types). With no Meta CAPI destination, the profile's single Meta Ads connection is used. - Added · #306 — The wiring read has a
platform_safeblock for Platform-Safe profiles:platform_safe_dataset: provisioned | missing(remediationprovision_platform_safe_dataset), only the Clean Dataset's rows inresults,recreate_ad_setinstead ofwire_optimization, and, on a live check, the ad sets to recreate with their target conversion and Ads Manager steps. It isnullin every other mode.
2026-09-22
- Added · #1653 — An email send can now START a thread, and the response says which thread it started.
POST /api/v1/messageswithchannel: "email"answers withthread_id(GmailthreadId/ OutlookconversationId— the same value the inboundmessage.receivedevent carries, so the thread you open is the thread the reply arrives on) andrfc822_message_id(theMessage-IDof what was sent). Sendto+ asubjectwith nothread_idand a NEW thread is opened; a reply returns the thread it replied into. The email content also takes an optionallist_unsubscribe: { url, one_click }, which writesList-Unsubscribe: <url>and — withone_click: true, which requires anhttps://url —List-Unsubscribe-Post: List-Unsubscribe=One-Click(RFC 8058). And the inbound email event gainsdelivery_failure:nullfor an ordinary message, or{ failed_recipient, status, original_rfc822_message_id }parsed out of the RFC 3464 delivery-status report a bounce carries, whereoriginal_rfc822_message_idequals therfc822_message_idyour send returned. Everything here is optional and additive — an existing consumer's request and response are unchanged.
2026-09-15
- Added · #1517 — The three message-send routes now honour an optional
Idempotency-Keyheader, so a send lost to a timeout or a 5xx can be retried without delivering twice. A repeat within 24 hours replays the first response verbatim and sends nothing; scope is your API key + the route + the key value. A repeat that arrives while the first send is still in flight answers409 idempotency_key_in_flight(a new error code — retry the same key with backoff) rather than risking a second delivery, and a repeat carrying a different body answers409 idempotency_key_conflict. Only a 2xx is stored, so a send that genuinely failed can be retried under the same key. Omit the header and nothing changes.
2026-09-14
- Added · #1420 —
GET /api/v1/partner/whatsapp/adslists a profile's click-to-WhatsApp ads with their destination (type, WhatsApp number, Page), call-to-action type, greeting andgreeting_status, paginated by keyset onad_id. Served from the nightly Meta sync, which now reads that configuration for every CTWA ad — so/partner/whatsapp/ads/{ad_id}/welcome-messageanswers for a synced ad without a Graph call, and still falls back to a live read for one launched since the last sync. No spend, no metrics, no PII.
2026-09-11
- Added · #1402 —
GET /api/v1/partner/whatsapp/ads/{ad_id}/welcome-messagereads the welcome message ("Saludo automático") a click-to-WhatsApp ad shows a customer BEFORE their first message — the greeting, the pre-filled text and the ice-breaker buttons. Meta never delivers it over the webhook.welcome: nullmeans the ad has none configured;404means Meta does not show us the ad,409that the profile has no usable Meta Ads connection.
2026-09-07
- Added · #452 — Per-consumer usage: request counts, the 4xx/5xx split and p50/p95 latency, per API key for a workspace and per registered app for Atribu ops. Every rate-limited response now also carries
X-RateLimit-Reset. - Changed · #452 — A registered consumer app now has a rate TIER (
standard300/min,elevated1200/min,internal6000/min) that sets its per-minute allowance on client-credential requests. Anatb_live_key keeps its ownrate_limit_per_minute— the two meter different principals and are not merged.
2026-09-06
- Added · #1209 —
GET /api/v1/openapi.jsonis served from the API host itself, so the spec and the API it describes cannot come from different deploys. - Added · #1190 —
DELETE /api/v1/workspaces/{workspaceId}archives a workspace, driven by membership. - Changed · #1185 — An OAuth app's empty
allowed_return_originsno longer means "no bounce": the effective allowlist derives from its ownredirect_uris.return_url_supportedonGET /api/v1/me's app-credential branch reports the effective set, and the admin routes returneffective_return_origins.
2026-09-05
- Breaking · #1159 — Two new scopes,
workspaces:writeandprofiles:write, both granted bymcp:write. An MCP user token minted with the defaultmcp:readgrant could create workspaces and profiles; it now gets403 insufficient_scopeand nothing is created on the way to that refusal. Sessions and app credentials are unchanged. - Breaking · #1154 — A webhook subscription URL must point at a public host; a private or non-routable target is refused at registration AND again at delivery time, and redirects are no longer followed.
- Added · #1109 — An agent can start a provider connect and hand the human a session-less URL. Stripe and MercadoPago join Meta Ads, Google Ads, Google Search Console and GoHighLevel on the shared
prepareConnectStartpath. - Added · #1099 — Every operation in the published spec carries structured
x-atribu-scopes,x-atribu-auth,x-atribu-idempotencyandx-atribu-scope-grainextensions, plus a response example per tag. - Added · #1089 — Every error envelope now carries
docs_url, pointing at that exact code's section of the generated errors reference. - Deprecated · #1086 —
POST /api/v1/goalsis deprecated in favour ofPOST /api/v1/goals/definitions, which can express what the older collection cannot (updates, an explicitconversion_key,lookback_window_days,is_default).
2026-09-03
- Added · #1000 —
GET /api/v1/workspaces/{workspaceId}/pii-access-log— who read this workspace's customer/visitor personal data, when, through which route and with what result. Durable for 365 days. - Changed · #502 —
DELETE /api/v1/connections/{id}now also revokes key generations older than the authorization that names them, when the authorization revoked is the app's last live one on that profile.revoked_keyscan exceed the number of keys you minted under it.
2026-09-02
- Added · #533 —
POST /api/v1/ads/{id}/creative-analysisqueues an on-demand creative analysis at the newcreatives:writescope. It never runs a model on the request; it answers202and theGETon the same path is the poll. - Added · #391 — Two commerce reads at the new
commerce:readscope: a connected store's catalogue (an ascending change feed you walk withupdated_since) and an order lookup by number, email or phone. The order row carries no customer identity.
2026-09-01
- Breaking · #898 —
GET /api/v1/overviewanswersspend: nullandroas: null— not"0"— when spend is unmeasurable for the requested filters. A measured zero is still"0". Guard the denominator; do not?? 0it. - Added · #675 — Eight classification writes at
goals:write. Every one of them changes the PAST: each queues a full-profile replay, so readreplay_queuedon the response —falsemeans the change is saved but existing sessions keep their old channels until a replay runs.