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 |
POST /api/v1/dashboard/chat | 2026-10-10 | 2027-01-08 | — |
POST /api/v1/workspaces/{workspaceId}/top-performers/chat | 2026-10-10 | 2027-01-08 | — |
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-10-15
- Changed · #2106 — The
order.status.changedwebhook (providershopify) fires for every order, not only shipped ones:statusis the order's current milestone (created,paid,fulfilled,delivered,refunded,cancelled), one event per (order, status).dataadds what a subscriber needs to match the order:attributes(cart / note attributes, so a cart permalink'sattributes[ref]comes back),lines[](variant_id,quantity,sku,price, …),financial_status,fulfillment_status,name,total_price,currency,customer_id,cart_token,checkout_token, timestamps, and the buyer'semail/phonewith fallbacks (null when Shopify withholds them). A consumer that only handlesfulfilled/deliveredshould ignore the other statuses. - Added · #2105 — A live Shopify read passthrough at the new
commerce:livescope: a connected store's catalogue (every variant, current stock) and its orders updated since a time (buyer email and phone, cart attributes, every line), read from Shopify in the request and never from Atribu's copy. Keyset-paginated, throttle-tolerant. The newcommerceconnect scope mintscommerce:read+commerce:live. - Added · #2104 — The connect hand-off accepts
shopify, with the store inshop(<handle>.myshopify.com, the bare handle or an admin URL). The merchant installs Atribu's Shopify app — or the custom-distribution app registered for that store — and returns to yourreturn_urlwith no Atribu login and no plan page. The connection's lifecycle reaches youratribusubscription asconnection.connected,connection.revoked(on uninstall) andconnection.reconnect_required, withdata.provider: "shopify". - Changed · #2101 —
kpis.repeat_rate_pct,kpis.median_days_to_second,repeat_purchase_histogramand the months after the first incohort_retentionnow count a REPEAT payment the wayGET /api/v1/overviewdoes: a payment that is not part of the customer's first purchase (is_first_payment = false— a balance charged within 7 days of the deposit, or another order of the same first purchase, is not a repeat). On one profile the repeat rate read 67 % where 1 of 6 new customers had actually paid again. - Fixed · #2101 —
credited_revenueon a touch is cash only: it no longer adds the estimated value of a credited appointment or lead (one customer showed USD 4,300 credited to a touch after paying USD 800). - Added · #2101 — Each row also returns
meta_reported_conversions(Meta's whole conversion figure for the entity) andmeta_reported_chats(its chats started), so a client can account for every part of Meta's number. - Changed · #2098 —
business_conversionsis now the canonical conversion count — the oneGET /api/v1/overviewreturns ascurrent.outcomesand Atribu's own screens print — over the lastdayscalendar days ending today in the profile's time zone. It used to count everyconversionsrow in the tracker's rolling window, including suppressed conversions (e.g. GoHighLevel SaaS recharges classified as not sold by ads), installments and refunds/disputes; on one profile it read 111 where the app read 86. - Fixed · #2098 —
signals.checkout_metadata.cash_events(and the split under it) counts the paymentsGET /api/v1/overviewcounts ascash_payments: a payment whose conversion is suppressed (e.g. a recharge not sold by ads) or an installment is no longer in it. - Fixed · #2098 —
/attribution/models: a campaign row'sconversion_count(and its lead / appointment / payment counts) counts a conversion only where the model gives it credit. A conversion was also counted in every other row where it had a zero-credit touch, so under a single-touch model (last_touch,first_touch,last_non_direct) the rows (andtotals) added up to more thanGET /api/v1/overview; they now add up to it exactly. Under a multi-touch model a conversion split between rows still counts in each, andcredited_conversionsis the share that adds up.
2026-10-14
- Added · #2075 — Meta analytics for one WhatsApp template:
GET /api/v1/whatsapp/templates/{name}/analytics?connection_id&language&start&endanswers Meta's own sent / delivered / read, clicks per button (quick-reply and URL buttons) and cost over UTC days, as totals and a daily series (range ≤ 90 days, cached 15 minutes). The first call on a WABA with template insights off switches them on and answers409 insights_enablingwithretry_after_seconds(422 insights_unsupportedon a WhatsApp Business app / coexistence account, where Meta refuses that switch);404 template_not_found,429on a Meta throttle,502 meta_unavailable. Every error envelope with aRetry-Afterheader now also carriesretry_after_seconds.
2026-10-11
- Added · #4145 — New webhook event
leadgen.received(provideratribu, opt-in): a Meta Instant Form submission on a Page the profile connected.datais the Graph lead merged over Meta's webhook value (leadgen_id,form_id,ad_id,campaign_id,field_data, …) plusprofile_id; the eventidis theleadgen_id. Same delivery as every partner event (HMAC, 24 h retry ladder). - Added · #2441 —
POST /api/v1/campaigns/{id}/budgetraises or lowers one campaign's own (Advantage) budget, daily or lifetime, in one audited write (campaigns:apply, a person on the record, ±50 % of the live budget per call, undo through the conversion-sync rollback). New codecampaign_not_editable(409).POST /api/v1/adsets/{id}/budgetalso acceptsnew_lifetime_budget_minorand answersbudget_type/previous_budget_minor/budget_minor.
2026-10-10
- Deprecated · #1764 — The in-app chat routes are retired.
POST /api/v1/dashboard/chatandPOST /api/v1/workspaces/{workspaceId}/top-performers/chatanswer410 feature_retiredwithDeprecation/Sunsetheaders until 2027-01-08, then 404. Ask the same questions from your own Claude, Claude Code or any MCP client connected tohttps://mcp.atribu.app: its tools are these same public reads. - Added · #1764 —
GET /api/v1/workspaces/{workspaceId}/reports/hubcarriescapabilities— whether this caller may use the hub's bulk schedule (can_edit) and bulk generate (can_apply), at the workspace grain those writes are checked at. - Added · #1764 —
GET /api/v1/conversion-sync/data-source-optionslists the profile's connectable Meta data sources (WhatsApp, Messenger, Instagram, website) andPOST /api/v1/conversion-sync/datasetscreates or retrieves the dataset for one of them and wires it as a Conversions API destination — the two steps of the console's «Connect a data source» wizard, now published.
2026-10-09
- Breaking · #1761 — Learned values are no longer bare numbers.
top_performer_likelihood,score.top_performer_likelihood_calibrated,hazard_30d_pause,hazard_30d_degradation,fatigue_risk_tier,expected_lifespan_daysandrisk_rank_scoreare{ value, status: served | withheld | not_applicable, reason }; readvalueonly whenstatusisserved. A model serves only after an out-of-time evaluation whose 95% CI clears chance — none does today, so these arewithheld./ads/fatigue-riskreturnsmodel.status = withheldand an emptyadslist. Rule verdicts gainrule: { id, thresholds, inputs }(composite_rule,fatigue_state_rule, scalingguidance.rule).
2026-10-08
- Added · #1886 —
POST …/data-sources/instagram/ensurerefuses an Instagram account connected through Instagram Login with409 instagram_facebook_login_required, before any Meta call: Meta creates Instagram datasets only for an account connected through Facebook Login for Business.GET …/data-sourcesreports it first as the instagram entry'sreason_code.
2026-10-07
- Added · #1886 — Instagram business-messaging lane:
POST /api/v1/conversion-sync/data-sources/instagram/ensurecreates the dataset linked to the profile's Instagram professional account (instagram_connection_required409 without one), and the data-sources status reports it. An event whoseclick_idscarrymessaging_channel: instagram+ig_sid+instagram_business_account_idis exported to that dataset asbusiness_messaging(Meta's Instagramuser_data); with no Instagram dataset it goes to the website pixel assystem_generatedwith those fields dropped. Every other event is exported as before. - Changed · #1885 —
outcome.recordednow delivers a healthcare-family profile's events to the app that provisioned the profile, and only to it — the webhook andGET /api/v1/outcomes/recordedwith that app's key; every other caller still gets nothing (403 healthcare_scope_forbiddenon the replay). That app gets the person when its grant on the profile opens contact, the webhook's usual rule, on both surfaces. - Added · #1885 —
outcome.recordedevents (webhook and replay) carrydata.outcome_event_idanddata.conversion_id— the idsGET /api/v1/campaigns/{id}/conversionsreturns — so an event joins to the campaign credited for it.conversion_idisnullwhile the outcome has produced no conversion.
2026-10-06
- Added · #1880 — WhatsApp templates with a media header:
header_media: { format: IMAGE | VIDEO | DOCUMENT, url }on the template create, mutually exclusive withheader_text. Atribu downloads the public https URL (JPEG/PNG ≤ 5 MB, MP4 ≤ 16 MB, PDF ≤ 100 MB) and uploads it to Meta as the review sample; a wrong type or unreachable URL is422 invalid_content, an oversized one413 payload_too_large. Each send carries its own media in aheadercomponent ({ type: 'image', image: { link } }), forwarded to Meta unchanged.
2026-10-05
- Added · #1876 —
platform_event_name_overrides.meta_capi_messagingon export rules: the event name for click-to-WhatsApp (business_messaging) sends, one of Meta's messaging names (e.g.LeadSubmitted,QualifiedLead,Purchase), ornullto keep the rule off that lane; anything else is a 422. Absent, Atribu maps by source event (appointment_booked→QualifiedLead).meta_capinow names the website event only. The WhatsApp test send usesLeadSubmitted.
2026-10-04
- Fixed · #1869 — Creating an export destination, or changing one's pixel / conversion action / connection, so that it matches another destination of the profile now answers
409 destination_conflict(new error code) withconflicting_destination_id, instead of a 500. Nothing is written. - Added · #1863 —
POST /api/v1/events/retractions(events:write): retract an event you ingested throughPOST /api/v1/events, by theidempotency_keyit was sent with. One transaction deletes the event, its conversions and their attribution, recomputes the customer's first-payment flags, queues the attribution recompute and records an audit entry naming your key andreason; the answer is counts only. Idempotent (status: "already_retracted", zero counts). Refused, deleting nothing, with409 event_already_exportedonce a conversion export exists, and409 event_linked_to_conversationwhen a conversation recorded the event as its lead; an unknown key is404 event_not_found. Never needs the attribution add-on. - Changed · #1838 — Customer-list consent v2 (
2026-10-04.customer-list.v2) is now the current version and is required to create a list. A v2 list uploads hashed e-mail, phone, first name, last name and country. A list created under v1 keeps uploading hashed e-mail and phone only, and re-POSTing its v1 acceptance stays idempotent. APOSTwith v2 on a live list re-records the consent.
2026-10-03
- Fixed · #1859 —
PUT /api/v1/profiles/{profileId}/audience-exclusionsno longer fails with500on batches of a few hundred rows. If Atribu's database cannot be reached mid-request it now answers503 database_unavailablewithRetry-After, never500. Retry the same body.
2026-10-01
- Added · #1841 —
GET /api/v1/attribution/ad-people: distinct people with an ad-tagged Touch per month, day or ad (profile time zone), split by origin (a partner's historical import, a conversation's ad referral, a website visit, other), beside Meta's own reported messaging conversations started. Counts only,analytics:read. - Added · #1841 —
messaging_conversations_started_7don eachGET /api/v1/ads/trendrow: Meta's own reported messaging conversations started per entity and day. - Added · #1838 — A Meta customer list of a Platform-Safe profile's paying customers (hashed e-mail and phone only) and its 1 % lookalike:
GET|POST|DELETE /api/v1/partner/audiences/customer-listandPOST …/lookalike. Off for every profile until Atribu enables it. Partner apps keep customers out of every audience withPUT /api/v1/profiles/{profileId}/audience-exclusions. - Fixed · #1838 — On a Platform-Safe profile, proposing or confirming a Conversion Sync audience now answers
409 platform_safe_audiences_disabledbefore any Meta call. It could create empty audiences in Meta before. - Added · #1140 —
GET /api/v1/conversion-sync/data-sourcesreports, per data source (WhatsApp, website pixel), whether the Meta dataset is wired, what blocks it and a 30-day preview of what it would export.POST …/data-sources/whatsapp/ensurecreates the WhatsApp Business Account's dataset (201, or 200 when it already exists);…/website/ensureattaches one of the ad account's EXISTING pixels (selectable_pixels) and never creates one. For an app-provisioned profile it is the only way that dataset gets created: connecting now only reportsready. Each data source reportsshared_with_profiles/warning: meta_destination_sharedwhen another profile already sends the same ad account or dataset to Meta. New error codewhatsapp_connection_required(409). - Changed · #1140 — A conversion declared
business_messaging(orchat) without a click-to-WhatsApp click id is no longer exported aschat: it goes to the website pixel assystem_generated(filteraction_source_degraded:business_messaging_to_system_generated). No event without actwa_clidis ever sent with a messagingaction_source. When the profile has no website pixel, such an event is not sent and the ledger now records it asskippedwithskip_reason: no_website_destinationinstead of dropping it silently.
2026-09-30
- Added · #1836 —
learning_stage_status(Meta's ad-set learning phase:LEARNING,SUCCESSorFAIL) andcampaign_objective(the parent campaign's objective) onGET /api/v1/ads/{id}?level=ad_set, read from Atribu's nightly-synced copy, never live from Meta. Null at every other level. - Fixed · #1831 —
api.atribu.appnow answers every operation that was served only onwww.atribu.app(the/api/v1/partner/*routes,POST /api/v1/events,/oauth/token, webhooks, calendar, email, Instagram and more) instead of404, by forwarding the request unchanged to the same handler.www.atribu.appremains the canonical base URL. - Fixed · #1828 —
outcome.recordedwebhooks are now enqueued in the replay's order within each delivered batch (they could be enqueued out of order inside a batch of up to 200). No stored data changed; the replay's order is unchanged. - Fixed · #1827 — Campaign conversions are paginated. The response used to stop at 50 conversions without saying so; it now takes
limit(1–200, default 50; anything else is a 400) andcursor, and returnspagination: { has_next, cursor }. Followcursorwhilehas_nextis true to read every conversion; a page can hold fewer thanlimitwhen paths are long. Without a cursor you get the same first page as before. - Added · #1827 — Each campaign conversion row carries
is_first_payment: whether the payment is the customer's first, the flag behind the first-payment figures. Null when the conversion is not a payment. Visible undercampaigns:read. - Breaking · #1825 — Campaign, ad set and ad counts now include only conversions CREDITED to the entity. Under the single-touch models (
last_touch,first_touch,last_non_direct) a conversion used to be counted under every campaign / ad set / ad its path crossed, and again in the unattributed remainder. Sooutcome_count,outcome_count_first_paymentandattributed_outcome_countgo DOWN for those models,cac/cac_first_paymentgo UP by the same ratio, andGET /api/v1/campaigns/unattributed's counts go down. Rows + remainder now add up to the attributed total. Values (outcome_value,roas) do not change. Underlinear,time_decayandatribua conversion split across two entities still counts under both. - Added · #1825 —
attributed_outcome_count(conversions of every kind credited to the entity, refunds excluded; not cash, never a ROAS/CAC denominator) andattributed_outcomes_by_type(the same count split by conversion type, e.g.{"dm_started": 89, "lead_created": 88}) onGET /api/v1/campaignsat everylevel,GET /api/v1/ads/{id}and theGET /api/v1/campaigns/unattributedremainder. A lead-gen or conversational account no longer reads a campaign row of zeros. - Added · #1824 — Each campaign conversion row now carries
outcome_event_id— the id Atribu returns at ingest — so a caller that stored it can join the drill back to its own record.conversion_idis a different id. Nullable.
2026-09-29
- Added · #1800 — Provider outcomes for partner apps. New webhook event
outcome.recorded(provideratribu, opt-in like every lifecycle event): each GoHighLevel and MercadoPago outcome recorded on a profile your app provisioned, with a stableid,data.provider,outcome_type,occurred_at,customer_key,value, andcontactonly when your app's grant carriescustomers:read. Never emitted for a healthcare-family profile. NewGET /api/v1/outcomes/recorded(conversions:readorcustomers:read) replays the same events in the same order from an opaque cursor (90-day window). - Changed · #1776 —
POST /api/v1/adsets/{id}/budgetrefuses an ad set that is ARCHIVED or DELETED on Meta, or whose campaign is, with the new409 adset_not_editable(status read live, Atribu's synced status only as a fallback). Nothing is written to Meta; the refused attempt is audited.
2026-09-28
- Breaking · #1800 — CRM stage mapping for partner keys. New
GET /api/v1/connections/{provider}/stages(gohighlevel;connections:readoranalytics:read;?locale=es|en): every pipeline stage with the suggested outcome, the mapping in force (current.source:suggested,userorengine_default), whether it can be written and, at the top,status,can_writeandnext_step. Write a stage withPATCH /api/v1/outcome-definitions/{id}{event_key}(goals:write, entitlement-gated, idempotent). Breaking (narrow): on apipeline_stagedefinition,event_keymust now be one of the read'soutcomes[].keyon both that PATCH andPUT /api/v1/profile/outcome-definitions; anything else is422 validation_error. - Changed · #1800 — CRM stages that are not outcomes stop producing outcome rows. A new GoHighLevel stage with no suggestion is stored as
unmapped(it used to becomelead_createdsilently). An opportunity in a stage mapped tounmappedorignored(including existing stages mapped toignored) records no outcome, so no conversion and no attribution, until the stage is mapped. Won / lost / abandoned deals are still recorded whatever their stage. No response shape changes; the visible effect is the customer journey timeline. Earlier rows are kept. - Added · #1800 — Reporting currency at provisioning.
POST /api/v1/profilesandPOST /api/v1/workspacesacceptreporting_currency(ISO-4217) for a new workspace; onPOST /api/v1/profilesit defaults to the request'scurrency. With neither, the workspace takes the currency of the first ad account connected to it instead of a silentUSD. Applied only when the call creates the workspace; change it later withPATCH /api/v1/workspaces/{workspaceId}/reporting-currency, which an ad-account connect never overrides. - Added · #1030 —
POST /api/v1/partner/ad-referrals: record the Meta ad a PAST click-to-message conversation came from, for a business moving to Atribu from another WhatsApp bot. Each referral becomes a touch atoccurred_at, so later money (closed_won,payment_received) is credited to the ad, only inside the profile's attribution windows. It creates no lead and never changes one. Nothing from the call is sent to Meta: the click id stays on the touch and is not stored on the customer. The ad must be one of the profile's own Meta ads (422 unknown_ad). Scoping and customer rules asPOST /api/v1/partner/declared-source; idempotent onidempotency_key.POST /api/v1/partner/ad-referrals/previewreports, read-only, how many existing conversions a batch of up to 500 would re-credit, per model.
2026-09-27
- Changed · #1800 — Connect hand-off hardening. One consent per hand-off (a double-click writes once, and the other tab normally reports the winner's outcome; confirm with the poll); a consent that stops partway on our side frees the link again after 3 minutes; a completed hand-off reports its real outcome on a second click; new
reasoninternal_error(not settled, retry the same link).POST /api/v1/connections/pending/{provider}/finalizeanswers409 account_connected_elsewhere(new error code) for an account live on another profile of the workspace, keeping the parked token.
2026-09-26
- Breaking · #1800 — Partner connect hand-off. A
connecthand-off now carriesstart_url, which goes straight to the provider's consent screen, and every ending returns your user to yourreturn_urlwithstatus(success,pending_selection,failed,cancelled), a machinereasonandhandoff_id. The samereasonlands onresult.reason. The/h/page shows your app's name and logo. Breaking: areturn_urlyour app is not registered for is now a400at the mint (it used to fail the hand-off later); a user who cancels on the provider's screen settles the hand-offcancelledwithuser_cancelled(wasfailedwithprovider_denied); a hand-off's bounce saysfailed/cancelledwhere it saiderror; andhandoff_unusableis nowhandoff_expiredorhandoff_used.
2026-09-25
- Changed · #1799 —
POST /api/v1/partner/declared-source:customeraccepts{ external_ref, email?, phone? }. An unknownexternal_refsent with an email and/or phone creates the customer from that PII and binds the ref (exactly asPOST /api/v1/eventsdoes foruser_traits.external_id), so answers can be backfilled for people Atribu has never seen; a ref alone still never creates one (422 unknown_customer_ref).wa_idis now accepted on healthcare-family profiles. A national id (RUT) is refused with400 invalid_parameter. - Added · #1782 —
privacy_mode(standard|platform_safe|hipaa) onPOST /api/v1/profilesand onPUT /api/v1/profiles/{profileId}/entitlements/atribu_attribution. It is written before the grant widens, so a healthcare profile's first attribution key is minted withoutcustomers:readandvisitors:read. Omitting it changes nothing. - Changed · #1782 — Policy S5 now covers keys that already exist. When a profile becomes healthcare-family (
privacy_modeplatform_safe/hipaa, or the workspace'sis_healthcare_agency), its live delegated keys carryingcustomers:readorvisitors:readare revoked. Mint a replacement withgrant_type=client_credentials; it comes back without both scopes. - Added · #1776 — Campaign pause and resume.
POST /api/v1/campaigns/{id}/pauseand/resumechange one campaign's status by its Meta id in ONE write — never a per-ad fan-out — withaffected_ads_count, the ledgeraction_idand a readyundocall in the answer (campaigns:apply,Idempotency-Keyrequired, audited). Both are idempotent on the end state (outcome: "no_change"). A resume after an Atribu pause is that pause's undo (rollback_of); ads paused on their own stay paused. New codescampaign_not_found,campaign_not_pausable,campaign_not_resumable.GET /api/v1/campaigns/{id}/statusreturns what a consent screen needs first — live status, the ads a pause would stop, daily budget, recent daily spend, each action's availability with areason, and aconfirm_tokento derive yourIdempotency-Keyfrom. Shared class codes can carryerror.reason. - Breaking · #1771 — Link tokens by your own customer key.
POST /api/v1/customers/link-tokensacceptscustomer_key(theuser_traits.external_idyou ingest) as a selector, and every result row now carriescustomer_key; a row that did not mint also carries areason(no_match,erased,no_selector,unknown_identifier_type,malformed_selector) and amessage. Newpartner_pagerecipe, and every recipe gainsrequires. Breaking for a key withoutcustomers:read: its rows no longer includecustomer_profile_id.identifier_typeis now one of five values (an unknown type makes the rowinvalidinstead ofnot_found), and a deployment without a token secret answers503 link_tokens_disabledinstead ofservice_unavailable. - Added · #1770 — Partner erasure by your own key.
POST /api/v1/customers/erasures(attribution:write) erases one customer of the key's profile named bycustomer_key,email,phoneor a pinned identifier, and answers a receipt:erasure_id,request_ref,status,customer_key,reason,requested_at,completed_at. Idempotent onrequest_ref(a replay answers200with the original receipt; the same ref for another customer is400). API keys only (a session gets403 api_key_required); never needs the attribution add-on. An unknown customer is404 customer_not_found, an unknown receipt404 erasure_not_found. Conversions stay in reports without a person; the key stops resolving and link tokens stop binding.GET /api/v1/customers/erasures/{erasure_id}reads the receipt. It never carriescustomer_profile_id. - Changed · #1768 — By-key journey misses get their own codes.
GET /api/v1/customers/journey?customer_key=answers404 customer_not_found(unknown, erased or another profile's customer, all the same) andGET /api/v1/conversions/{id}/journeyanswers404 conversion_not_found, instead ofnot_found, which also means a route the deployment does not serve. Same status; branch on the code. - Added · #1708 —
POST /api/v1/partner/declared-source: a partner that asks "how did you find us?" in its own WhatsApp conversation sends the interpreted answer, stored beside click attribution withcollected_via = partner. Name the customer by exactly one ofexternal_ref(your own customer id, resolved read-only — required on healthcare-family profiles) orwa_id. Idempotent onwamid, one answer per customer, first wins; an unknownanswer_keyis422 unknown_answer_key, an unknownexternal_refis422 unknown_customer_ref. The declared-source vocabulary gainswalk_in(walked or drove past the place).
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.