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 | — |
GET /api/v1/conversion-sync/wiring/candidate-ad-sets | 2026-10-21 | not scheduled | /api/v1/conversion-sync/wiring/campaigns |
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-28
- Added · #2381 — Audio on
POST /api/v1/messages:content.voice: truesends an OGG/Opus audio as a native WhatsApp voice note (any other format stays a plain audio message), and Instagram now acceptstype: "audio"withmedia.link(aac, m4a, wav or mp4, 25 MB) instead of answering 422. - Added · #2364 —
user_traits.instagram_scoped_idon outcome ingest: the buyer's Instagram-scoped id (IGSID) on your connected Instagram account. It joins your customer to their Instagram DMs and to the click-to-Instagram-Direct ad that opened them. Unlikeexternal_idit may create the customer on its own, and a DM-only customer (no email, no phone, no payments) holding that IGSID is merged into the one the event resolves to. Re-send an event with its original idempotency key to move it onto that customer. - Changed · #2307 —
POST /api/v1/ad-builder/ad-sets:location_types_honoredis now alsotruewhen Meta stores["home", "frequently_in"](any order), people who live in the area or are regularly there. With Advantage+ audience on, Meta stores that set for the["home"]the ad builder sends.recent,travel_inor a set withouthomestill readsfalse. The request is unchanged.
2026-10-27
- Added · #2379 —
GET /api/v1/ad-account/pixelslists the ad account's pixels and marks the one the website Conversions API destination sends to.POST /api/v1/ad-account/pixelscreates a named website pixel on the ad account (or adopts the one already named so) and wires it as that destination — one audited Meta write, not reversible. For an app's «create pixel» button (campaigns:apply+exports:write). - Added · #2338 — New
recurring_payment_modevalue on Sync Rules:first_per_rule. The rule sends only a person's FIRST conversion it matches — the earliest of their outcomes with the rule's event types that passes its booking, amount and product filters, whether or not that one was sent — and holds every later one asskip_reason: "repeat_for_rule". Unlikeskip, it ignoresis_first_payment: with one rule per product (a trial, then a plan), the plan bought after the trial is that person's first plan and ships, while a second plan or a subscription renewal is held. Same person = the customer profile, else the payer key (never Mercado Pago's in-person placeholder payer); a household's other patient (subject_ref) is another person. - Added · #2334 —
POST /api/v1/eventsaccepts optionalcontents([{ id, quantity?, item_price? }]) andcontent_type(product|product_group) on a sale outcome. Atribu sends them to Meta ascustom_data.contents, derivescontent_idsandnum_items, and mapscontentsfor TikTok. No product catalog is needed: the ids are whatever was sold. Meta's data-quality checks flag a Purchase without them. Omitting them changes nothing. - Added · #2334 —
POST /api/v1/eventsaccepts an optionalpayment_source(manual|gateway) on a payment outcome. Amanualpayment (staff entry, back-office sync, a bank-notice match a person confirmed) is held 6 hours from when Atribu recorded it before it is sent to Meta, and is never sent if a reversal of the same sale (payment_refundedwithproperties.original_sale_external_id) arrives inside the hold; its deliveries entry readsskippedwithskip_reason: payment_reversed_in_hold. Agatewayor undeclared payment is sent as before. Revenue, attribution and the dashboard are unaffected. - Added · #2333 —
user_traits.full_nameon outcome ingest. Send the buyer's name as one string when you hold no first/last split (e.g. a WhatsApp display name). Atribu trims it, collapses whitespace, drops tokens with no letter and splits it by word count for the customer profile and Meta'sfn/ln: 1 word → first name only; 2 → first + last; 3 → first name only; 4 → first + the third word as last (Juan Pablo Pérez González→ Juan / Pérez); 5 or more → first name only. An explicitfirst_name/last_namewins over the split, each on its own. Also accepted on Webpay'scustomer. - Added · #2327 — Product filter on Sync Rules. New
source_config.description_patterns(string[], up to 50 patterns of up to 200 characters) on a rule over a conversion that carries an amount (revenue_typecashorgross): the conversion ships only when a pattern matches the payment's product text —raw_data.descriptionor a Mercado Pagoadditional_info.items[].title. Case- and accent-insensitive; a plain pattern matches as a substring, one with*/?as a glob over the whole text. A payment that matches none, or has no product text, is held asdescription_not_matched. Combines with the amount filter; the preview applies it; 422 on a malformed list, a pipeline rule or a definition with no amount. - Added · #2326 — Sync Rules never resend history when they change. Each rule now carries
export_from: set when it is created and moved to the moment of any change to what it sends (its conversion, trigger mode, event names, destinations or anysource_configfilter, re-enabling or un-archiving), also for changes made outside the API. A conversion before it is held asbefore_rule_start;backfill_days(0–7) on a create or update opts into that many days of history. Newsource_config.booking_sources(e.g.["booking_widget"]) sends only bookings made on those GHL sources — a tracker widget booking andslot_selectedcount asbooking_widget— held otherwise asbooking_source_not_selected. Newsource_config.require_consented_site_visit(+ optionalsite_hosts) sends a conversion only when the same person had a browser-tracker event with an ads-consent grant on those hosts before it, held otherwise asno_consented_site_visit. Preview counts apply both filters; 422 on a malformed value or on a pipeline rule. - Added · #2321 — Amount filter on Sync Rules over a conversion that carries an amount (revenue_type
cashorgross, e.g.payment_received):source_config.value_min/value_max(inclusive),value_in(only these exact amounts),value_not_in(never these) andvalue_currency(ISO 4217), compared with the conversion's original amount in its original currency, so one payment conversion can feed StartTrial, Purchase and Subscribe by price. A conversion with no amount never matches. A filtered payment is held asamount_out_of_range/amount_currency_mismatch/amount_missing. Combines with the booking filter; preview counts apply it; 422 whenvalue_min>value_max, a value is not a finite number, or the definition carries no amount. - Added · #2303 — Booking filter on Sync Rules over a booking conversion (
appointment_booked,slot_selected):source_config.calendar_idssends only bookings on those calendars (empty or absent = all), andsource_config.exclude_staff_created: truedrops bookings a staff member made inside GHL (sourcemobile_app,contactdetails_page,opportunity_page,google_calendar). A filtered booking is held asbooking_calendar_not_selected/booking_staff_created. NewGET /api/v1/exports/rules/booking-calendarslists the calendars the profile booked on in the last 90 days, with a label and counts. - Breaking · #2302 — MCP
get_funnelno longer walks the fixed lead chain (lead_created → appointment_booked → showed → qualified → closed_won, event counts). It answers the profile's own Funnel: its saved chain of Funnel Steps, else the default chain, counting people credited to their Entry Ad — the same numbers asGET /api/v1/ads/funnel. Withoutbreakdownit returns the profile-widetotal;breakdown=campaign,ad_set,adorangleadds one group per entity, with «Sin anuncio» and «Anuncio sin identificar» apart andmeasurement_incompletesteps listed.main_chain,drop_off_outcomes,totalsand thescopeargument are gone; the lead-stage counts stay onGET /api/v1/goals/outcome-counts.GET /api/v1/ads/funnelandget_funnelgroups now carrynameat every level: the campaign, ad set or ad name the /ads list shows (or the Angle's name), null for the two baselines and for an entity with no name in the ad catalog. - Added · #2300 —
GET /api/v1/ads/angles: Angles group a profile's ads by the idea they pitch (a testimonial, avoiding surgery, a trial-session offer). Atribu proposes them from each ad's creative analysis inside the daily AI allowance, and new ads join one automatically.POST /api/v1/ads/angles,PATCH …/angles/{id},POST …/angles/{id}/mergeandPOST …/angles/{id}/adscreate, rename, merge and move; a later re-cluster never undoes them.GET /api/v1/ads/funnel?level=angleanswers the Funnel per Angle. - Added · #2299 —
GET /api/v1/ads/funnel: each group carriesunidentified_entry_ad. With noids, people whose Entry Ad matches no campaign, ad set or ad of the profile are counted in one group withunidentified_entry_ad: true(«Anuncio sin identificar»), next to «Sin anuncio», instead of under a key no entity has. An Entry Ad that carries an ad name where the id belongs resolves to the ad with that exact name when it is unique in its ad set, else in the profile. - Added · #2298 —
GET,PUTandDELETE /api/v1/ads/funnel/steps: each profile can save its own Funnel — reorder, rename, remove or add steps from the events and conversions it tracks — and restore the suggested chain. Once saved,GET /api/v1/ads/funneland every other Funnel surface count that chain (chain_source: saved); a profile that never edits keeps the default chain, which updates as new events start being tracked. A saved step whose event stopped being tracked counts 0, never an error. - Added · #2297 —
GET /api/v1/ads/funnel: how far the people each campaign, ad set or ad brought got on the site, step by step — people who Reached each Funnel Step, step rate, Drop-off, cost per step and the biggest Drop-off, plus a «Sin anuncio» group for people with no Entry Ad. Each person is credited to the ad whose click brought them in, not to an attribution model, so the last step will not match attributed results. The chain is built from what the profile recorded in the last 30 days.
2026-10-26
- Added · #2307 —
POST /api/v1/ad-builder/objects/{id}/activateswitches ON (PAUSED → ACTIVE) one campaign, ad set or ad the ad builder created — right after the create, before the structure sync has seen it. Already ACTIVE isno_change; any other status is409 invalid_state. Idempotent onIdempotency-Key, audited, and the ad-builder rollback pauses it again (campaigns:apply). - Added · #2307 — The ad-builder primitives: upload images and videos, then create a campaign (ABO testing or CBO Winners, house naming), an ad set (website or click-to-WhatsApp, home-only geo), a creative (4:5 + 9:16 placement assets, or click-to-WhatsApp) and an ad (dataset pinned) — every object PAUSED, audited, idempotent on
Idempotency-Key, with a delete-rollback per create (campaigns:apply).GET /api/v1/ad-builder/objects/{id}says whether an object was edited outside since it was created, andGET /api/v1/ad-accountreturns Meta's daily spending limit, today's spend and the account cap. - Added · #2271 — Send Meta only attributed leads, profile-wide.
GET /api/v1/exports/meta-attribution-scopereports whether the profile's Meta conversion rules useplatform_eligibility_mode: attributed_to_platform(state:attributed,allormixed);PUTwith{ attributed_only }moves every one of them through the rule update path (owner or admin only; rules onany_touch_from_platformare never moved) and stores the choice. While the switch is ON, a Meta-enabled conversion rule created withPOST /api/v1/exports/rulesor/rules/bulkwithoutplatform_eligibility_modedefaults toattributed_to_platform, notall; naming a mode always wins.GET /api/v1/exports/whatsapp-unattributed-leadscounts the last 30 days' WhatsApp-origin leads no ad of the profile's Meta ad account explains, and whether WhatsApp is connected. - Added · #2262 —
POST /api/v1/eventsanswers402 collection_paused(a new error code) when the workspace's data collection is paused because its 14-day trial ended without a plan. Nothing is recorded; do not retry until a plan is chosen in Billing, then re-send. Workspaces with a plan, and every workspace that existed before the change, are unaffected. - Changed · #2204 — Clean Dataset provisioning runs one at a time per profile: a concurrent call waits for the running one and answers
already_provisioned, or409 provision_in_progress(a new error code, retryable afterRetry-After) when it does not finish in time — so two calls can no longer create two datasets. When the business that owns the ad account refuses the connecting Facebook user (Metacode 10, MANAGE_PIXELS), Atribu now tries that user's own business (an agency's) if it can reach the ad account, creating the dataset there and sharing it with the ad account; when that is not possible,action_requiredcarries the newreason: "business_permission_missing"and abusiness: { id, name }naming the business that refused (nullon other reasons). - Changed · #2204 — Health profiles: DPA first, Clean means only Clean, and sending is an explicit switch. Every health «yes» (
health-data-answerwithplatform_safe_clean,platform_safe_existingorhipaa, andplatform-safe/target) answers409 dpa_requireduntil the profile's DPA is accepted.platform-safe/targetwithcleannow sets the target tocleaneven when the provision answersaction_required— the profile sends Meta nothing until a Clean Dataset exists, instead of staying on its existing dataset. A provision that creates the profile's first Clean Dataset starts with Meta sending OFF (checks.meta_sending_enabled: false); the newPOST /api/v1/conversion-sync/platform-safe/sending({ enabled }) turns it on — a signed-in owner or admin only, never an API key — once the Clean Dataset exists and the DPA is accepted.PUT /api/v1/conversion-sync/catalogcan no longer turn a Platform-Safe or HIPAA profile's sending on (409 invalid_state).
2026-10-23
- Changed · #2223 — A
platform_safeorhipaaprofile sends NO URL to Meta or TikTok: noevent_source_url, not even the origin, on every event,websiteevents included.privacy_config.url_sanitizationacceptsomit; in those two modesPUT /api/v1/conversion-sync/catalogfloors any other value toomitand the catalog returnsomit. Meta accepts website events without the URL; matching relies onuser_dataand fbc/fbp. Standard and custom profiles are unchanged. - Breaking · #2208 —
GET/POST /api/v1/conversion-sync/platform-safe/repointandPOST /api/v1/conversion-sync/platform-safe/go-liveare DELETED (not deprecated) and answer404. Ad sets move onto a Clean Dataset — from any Source dataset — through Conectar campañas:GET /api/v1/conversion-sync/wiring/campaignsandPOST /api/v1/conversion-sync/wiring/ad-sets.POST …/platform-safe/targetstill listsad_sets_to_recreate. SDK@atribu/node4.0.0 dropsplatformSafeRepointPreview,applyPlatformSafeRepointandstartPlatformSafeGoLive. - Breaking · #2207 — Every Recreate asks what happens to the ORIGINAL ad set.
pause_original(truepauses it once the copy is live,falseleaves it running) is REQUIRED, with no default, for every ad set a request recreates — a missing answer is400 validation_errornaming the ad sets, nothing queued. Keyed by ad set id onwiring/ad-sets, a field on eachad_sets[]row onrewire.duplicate_swap_modeis retired (400). In-place edits need no answer.
2026-10-22
- Changed · #2201 — Meta Conversion Sync ships ONE event per sale: when outcomes carry the same
properties.order_id, only the first event of a given name reaches each destination (e.g.closed_wonandpayment_receivedboth mapped toPurchasesend one Purchase). The others are held asorder_already_sent/order_duplicate_in_batch; an outcome withoutorder_idships as before. New rule settingsource_config.closed_won_requires_payment(defaulttrue):falselets that rule'sclosed_wonship to Meta and Google without a confirmed payment. - Changed · #2197 — A declared
action_source: system_generatedis refined by Atribu's own evidence on Meta export: when Atribu's tag captured the same event on the site (the outcome's event id is the browser event's id) it ships aswebsitewith the page URL; when the buyer came from a click-to-WhatsApp ad and the event has no web footprint it goes ONLY to the WhatsApp dataset asbusiness_messaging. Ledger filtersaction_source_refined:*. Every other declared value is unchanged. - Added · #2173 —
GET /api/v1/conversion-sync/wiring/campaignstakesedit_mode=skipto answer without the validate_only probes (every offerable ad set readsedit_mode: unknown,probe_limit: 0) in a fraction of the time. NewGET /api/v1/conversion-sync/wiring/campaigns/edit-modes?destination_id=&rule_id=(exports:read) runs those probes for one rule. Withoutedit_modethe read is unchanged.
2026-10-21
- Breaking · #2154 —
POST /api/v1/exports/testnow REQUIREStest_event_codeand answers422 validation_errorwithout one, before Meta is called. Without the code Meta filed the event on the live dataset as a real conversion. Copy the code from Events Manager → Test Events.
2026-10-15
- Added · #2117 — New
GET /api/v1/conversions/attribution(analytics:readorcampaigns:read): every conversion of a window with the campaign that wins its credit — highestcredited_weight, a direct credit beating an inherited one on a tie — in one keyset-paginated walk (limitup to 1000, opaquecursor). Replaces readingGET /api/v1/campaigns/{id}/conversionsonce per campaign and folding the rows; that route is unchanged. - Added · #2116 — Two webhook events, provider
shopify:customer.redacted(Shopify'scustomers/redact;data=shop,customer_id,email,phone,orders_to_redact) andshop.redacted(Shopify'sshop/redact;data=shop). Atribu erases its own copy, then forwards the request so an app that stored buyer data from the store's order events erases it too. Opt-in: add them to a subscription'seventswith providershopify. - 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.