Atribu
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

OperationDeprecatedSunsetSuccessor
POST /api/v1/goals2026-09-052026-12-04/api/v1/goals/definitions

Changes

Each line links the issue that carries the full reasoning — what broke, what to change, and why the decision went the way it did.

2026-09-24

  • Added · #1766 — Journey reads for keys without customers:read. GET /api/v1/conversions/{id}/journey now opens at conversions:read: a key without customers:read gets the key-only projection (touch, channel, campaign / ad set / ad by platform id, click-id kind, join method, credit, in_window) with meta.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 the external_id you ingest, with meta.person (counts and the newest 50 conversions); ?email= / ?phone= need customers:read. Healthcare-family profiles' keys now also leave out visitors:read (policy S5).
  • Added · #1696 — Apply Atribu's URL parameters to Meta ads. POST /api/v1/quality/ad-ids/url-tags/apply writes them onto up to 25 of the ads GET /api/v1/quality/ad-ids lists, merged with each ad's own, and returns one result per ad (applied, skipped with a reason, or Meta's refusal). Every call must carry acknowledge_learning_reset: true: the change replaces the ad's creative (same post), which Meta treats as a significant edit. …/undo puts one ad back on its previous creative; GET …/url-tags says 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}/pause and /resume pause or resume one ad by its Meta id with no recommendation behind it, and POST /api/v1/adsets/{id}/budget changes one ad set's daily budget (±50 % of the live budget per call, 422 budget_change_out_of_bounds outside it) — all campaigns:apply, Idempotency-Key required, audited and reversible. A connection whose Meta grant lacks ads_management answers 403 meta_write_permission_missing before anything is written. GET /api/v1/conversions/{id}/journey returns the visitor timeline behind one conversion, addressed by its outcome_event_id, conversions.id or your own event key.
  • Changed · #1676 — On a key minted by the app that provisioned the profile, applied_by may be omitted: the apply is attributed to the workspace's owner (the user provisioning created) while it is still the active owner. An explicit applied_by, and every other key, behave as before.

2026-09-23

  • Added · #1670 — POST /api/v1/events accepts an optional action_source in 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 than website ships with no event_source_url — even when the buyer has fbc/fbp browser signals. A business_messaging event with click_ids.ctwa_clid goes to the dataset linked to the profile's WhatsApp Business Account; without that dataset or a click id it is sent as chat. Omitting the field keeps the previous inference.
  • Added · #1670 — Each deliveries ledger entry now carries action_source: the Meta action_source the stored payload carried (the declared value when there was one, else Atribu's inference). null for 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 on GET /api/v1/exports/destinations) decides which dataset gets the scrubbed feed: under existing the ordinary Meta CAPI destinations ship and nothing is recreated; under clean only the Clean Dataset ships, as before. Profiles that already had a Clean Dataset were moved to clean, so their routing is unchanged. A successful provision sets the target to clean, and its checks report it. Deliveries held for a leftover Clean Dataset under existing are skipped with platform_safe_clean_dataset_inactive.
  • Added · #1669 — POST /api/v1/conversion-sync/platform-safe/target switches a Platform-Safe profile between its existing dataset and a Clean Dataset. clean provisions the Clean Dataset and lists the ad sets to recreate (the target flips only when a Clean Dataset exists); going back to existing answers 409 clean_dataset_has_live_ad_sets while Clean Dataset ad sets still deliver, unless confirm_live_ad_sets: true. The wiring read's platform_safe block gains target, 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 open platform_safe_events_not_accepted alert) and the switch_to_clean_dataset remediation; under existing it 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: accepted with that version and acceptor, so sign_dpa no longer appears for them. Withdrawing returns those inherited profiles to withdrawn; 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 (with reconnect_required: true) or 429 quota_exceeded — a refused recipient answers 422 recipient_rejected, and every one carries the provider's own words in provider, provider_status, provider_code and provider_message. The connection reports status: "needs_action" (or reconnect_required for a dead grant) with the provider's message in status_reason, and a channel.health.updated event is pushed on provider email; the first send the provider accepts clears it. Only a transient provider failure still answers 502/503.
  • Fixed · #1653 — Inbound email delivery_failure now carries the report's status on Gmail bounces — it was always null there, because Gmail nests the per-recipient fields one part deeper than Atribu read, so a permanent 5.x.x looked like a soft bounce. It also gains action (failed, delayed, delivered, relayed or expanded) and diagnostic_code (the remote server's own words), both null when 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_id and rfc822_message_id are unchanged. Inbound Outlook events keep Graph's default-format id, so recognise your own send by rfc822_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: true or confirm_duplicate_swap: true must also send consent: { 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 is 409 consent_required and nothing reaches Meta. Signed-in sessions are unchanged. Both responses gain warnings (new ad set ids, learning reset, double spend), and error envelopes may carry consent.
  • Changed · #306 — Platform-Safe provisioning is ready for partners. An API key or MCP user token must switch the profile to platform_safe first, or gets 409 platform_safe_required; no usable Meta Ads connection is 409 meta_connection_required. A Clean Dataset that already covers every tracked outcome answers status: "already_provisioned" with no Meta call, and every 200 now carries checks (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_safe block for Platform-Safe profiles: platform_safe_dataset: provisioned | missing (remediation provision_platform_safe_dataset), only the Clean Dataset's rows in results, recreate_ad_set instead of wire_optimization, and, on a live check, the ad sets to recreate with their target conversion and Ads Manager steps. It is null in 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/messages with channel: "email" answers with thread_id (Gmail threadId / Outlook conversationId — the same value the inbound message.received event carries, so the thread you open is the thread the reply arrives on) and rfc822_message_id (the Message-ID of what was sent). Send to + a subject with no thread_id and a NEW thread is opened; a reply returns the thread it replied into. The email content also takes an optional list_unsubscribe: { url, one_click }, which writes List-Unsubscribe: <url> and — with one_click: true, which requires an https:// url — List-Unsubscribe-Post: List-Unsubscribe=One-Click (RFC 8058). And the inbound email event gains delivery_failure: null for an ordinary message, or { failed_recipient, status, original_rfc822_message_id } parsed out of the RFC 3464 delivery-status report a bounce carries, where original_rfc822_message_id equals the rfc822_message_id your 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-Key header, 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 answers 409 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 answers 409 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/ads lists a profile's click-to-WhatsApp ads with their destination (type, WhatsApp number, Page), call-to-action type, greeting and greeting_status, paginated by keyset on ad_id. Served from the nightly Meta sync, which now reads that configuration for every CTWA ad — so /partner/whatsapp/ads/{ad_id}/welcome-message answers 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-message reads 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: null means the ad has none configured; 404 means Meta does not show us the ad, 409 that 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 (standard 300/min, elevated 1200/min, internal 6000/min) that sets its per-minute allowance on client-credential requests. An atb_live_ key keeps its own rate_limit_per_minute — the two meter different principals and are not merged.

2026-09-06

  • Added · #1209 — GET /api/v1/openapi.json is 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_origins no longer means "no bounce": the effective allowlist derives from its own redirect_uris. return_url_supported on GET /api/v1/me's app-credential branch reports the effective set, and the admin routes return effective_return_origins.

2026-09-05

  • Breaking · #1159 — Two new scopes, workspaces:write and profiles:write, both granted by mcp:write. An MCP user token minted with the default mcp:read grant could create workspaces and profiles; it now gets 403 insufficient_scope and 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 prepareConnectStart path.
  • Added · #1099 — Every operation in the published spec carries structured x-atribu-scopes, x-atribu-auth, x-atribu-idempotency and x-atribu-scope-grain extensions, 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/goals is deprecated in favour of POST /api/v1/goals/definitions, which can express what the older collection cannot (updates, an explicit conversion_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_keys can exceed the number of keys you minted under it.

2026-09-02

  • Added · #533 — POST /api/v1/ads/{id}/creative-analysis queues an on-demand creative analysis at the new creatives:write scope. It never runs a model on the request; it answers 202 and the GET on the same path is the poll.
  • Added · #391 — Two commerce reads at the new commerce:read scope: a connected store's catalogue (an ascending change feed you walk with updated_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/overview answers spend: null and roas: null — not "0" — when spend is unmeasurable for the requested filters. A measured zero is still "0". Guard the denominator; do not ?? 0 it.
  • Added · #675 — Eight classification writes at goals:write. Every one of them changes the PAST: each queues a full-profile replay, so read replay_queued on the response — false means the change is saved but existing sessions keep their old channels until a replay runs.

On this page