Atribu
API Reference

Errors

API error codes, HTTP status, and what to do about each one

Every error response shares one envelope, on both the Hono and Next-served /api/v1/** surfaces.

Error response
{
  "error": {
    "code": "insufficient_scope",
    "message": "This API key does not have the 'campaigns:read' scope",
    "status": 403,
    "request_id": "req_a1b2c3d4",
    "docs_url": "https://www.atribu.app/docs/api/errors#insufficient_scope",
    "required_scopes": ["campaigns:read"]
  }
}

Envelope fields

  • code — the machine-readable code — see the table below for the full catalogue.
  • docs_url — always present. Deep link to the section on this page for this exact code.
  • required_scopes — present on insufficient_scope when the missing scope(s) are known by name. Mint a new key with one of these, or see Authentication.
  • reconnect_required / reconnect_url — present when a connection's provider token was revoked and the caller must re-authorize.

Error codes

unauthorized — 401

The request carries no valid credential — a missing, malformed, expired, or revoked API key or session. Include a valid Bearer token in the Authorization header.


forbidden — 403

The credential is valid but not allowed to perform this action (for example, a workspace role that lacks the required permission).


insufficient_scope — 403

The API key or session does not carry a scope this route requires. When the missing scope is known, required_scopes names it — mint a new key with that scope, or have a workspace admin grant a role that includes it.


not_found — 404

The requested resource does not exist, or exists but is outside what this credential can read.


invalid_parameter — 400

A query parameter or path segment failed validation (wrong type, a malformed UUID, or a value out of range).


invalid_request — 400

The request itself is malformed — invalid JSON, the wrong content type, or a shape the endpoint cannot parse.


validation_error — 422

The request body parsed but failed schema validation. message names the offending field.


invalid_content — 400

The request body's content — not just its shape — is invalid (for example, an empty attachment or the wrong multipart part).


invalid_date_range — 400

date_from/date_to failed the shared date-range contract: both required, YYYY-MM-DD, real calendar dates, date_from on or before date_to, and within the maximum span.


entitlement_required — 409

The profile was provisioned by a consumer app that has not turned Atribu's attribution features on for it, so this write is not available. The app that provisioned the profile enables it with PUT /api/v1/profiles/{profileId}/entitlements/atribu_attribution ({"enabled": true}). Reads are unaffected, and a profile created directly in Atribu never sees this error.


healthcare_scope_forbidden — 403

The profile belongs to a healthcare business (its workspace is marked as a healthcare agency, or the profile runs in platform_safe or hipaa privacy mode), and a partner app's grant for such a profile never carries customers:read or visitors:read, the person-level and per-visitor reads. Request analytics instead of analytics_pii. Keys minted for these profiles leave both out; read the projected journeys (GET /api/v1/conversions/{id}/journey, GET /api/v1/customers/journey?customer_key=) under conversions:read instead. Everything else in the attribution bundle, such as campaigns:read, exports:read and campaigns:apply, stays.


invalid_state — 409

The resource is not in a state that allows this operation (for example, a duplicate name, or an invitation already accepted or revoked).


rate_limit_exceeded — 429

The caller exceeded its rate limit. Respect the Retry-After header before retrying.


connection_not_ready — 409

The named data connection exists but is not yet usable (for example, it is missing an access token).


provider_error — 502

An upstream provider (Meta, GoHighLevel, Gmail, Microsoft Graph, …) returned an error while fulfilling this request. 502 (or 503) means the failure is transient — the provider was unreachable, timed out or answered 5xx — and a retry may succeed. On an email send, a provider refusal Atribu has no more specific code for answers 422 with this code instead, carrying the provider's own words in provider_code / provider_message: the provider read the request and said no, so the identical retry will fail the same way.


account_suspended — 403

Email send: the mailbox's provider has suspended the account (for example Outlook.com ErrorAccountSuspend), so it cannot send anything until its owner acts at the provider — provider_message carries the provider's instructions. Do not retry. The connection now reports status: "needs_action" and a channel.health.updated event was pushed; the first send the provider accepts again clears it.


auth_revoked — 403

Email send: the mailbox's OAuth grant is revoked, expired or missing the send permission (Microsoft Graph 401, Gmail 401, or invalid_grant when refreshing). Carries reconnect_required: true. The connection is marked reconnect_required and a connection.reconnect_required / channel.health.updated event was pushed; reconnect the mailbox through the connect hand-off.


mailbox_disabled — 403

Email send: the account has no usable mailbox, or mail is turned off for it (for example Gmail Mail service not enabled, or a disabled Microsoft 365 mailbox). The owner or their tenant administrator must enable it. Do not retry; the connection reports status: "needs_action" until a send succeeds.


quota_exceeded — 429

Email send: the mailbox hit its own quota at the provider — its storage is full, or it reached the provider's daily sending limit. Respect Retry-After; if storage is full the owner has to free space. The connection reports status: "needs_action" until a send succeeds. Distinct from rate_limit_exceeded, which is Atribu's own per-mailbox cap or provider throttling.


recipient_rejected — 422

Email send: the provider refused this recipient (an invalid or unknown address). About this message only — other sends through the mailbox are unaffected and its health is not changed. Fix the address before retrying.


service_unavailable — 503

A dependency this endpoint needs is not configured, or is temporarily down.


not_implemented — 501

This capability is not available on this deployment, or not yet for this provider.


internal_error — 500

An unexpected server error. request_id identifies it for support.


calendar_scope_required — 403

The connection's Google grant is missing the calendar scope this operation needs (calendar.app.created to manage calendars, calendar.acls to share).


calendar_unsupported — 422

The request targeted a calendar Atribu doesn't manage (for example primary, or an unknown id). Booking operations only run on Atribu-created calendars.


provider_not_configured — 501

A read-only external-calendar provider (Microsoft) has no app registration on this deployment, so the endpoint cannot serve it yet.


invalid_window — 400

The requested read-events time window exceeds the endpoint's 90-day cap.


media_expired — 410

The referenced media id has expired or the provider no longer serves it. WhatsApp webhook media ids expire after 7 days.


whatsapp_register_limit — 429

WhatsApp phone-number registration is capped at 10 attempts per number per 72 hours. This is propagated verbatim from Meta and is never retried server-side — wait out the window.


whatsapp_payment_required — 402

The WhatsApp Business Account has no attached payment method, or its funding source was declined.


whatsapp_otp_relay_unavailable — 503

The OTP-capture relay's store is unreachable, so a captured code cannot be held for the connect page.


audit_timeout — 504

The on-demand UTM-health scan did not finish inside the request budget (Postgres cancelled the statement, or the gateway gave up first). The previously cached summary is still readable; retry the scan later.


weak_password — 422

The new password is shorter than the minimum required length.


compromised_password — 422

The new password appears in a public breach corpus (HaveIBeenPwned k-anonymity check).


current_password_required — 400

The caller has an email/password identity, so proving the current password is required, and the request omitted it.


current_password_invalid — 400

The supplied current password did not verify against the caller's identity.


plan_upgrade_required — 402

This workspace-admin write needs a plan this workspace is not on. The remedy is upgrading the plan, not a different role.


whatsapp_service_window_expired — 422

The WhatsApp customer-service window (24 hours since the customer's last message) has closed. Use a template or interactive message instead.


payload_too_large — 413

The uploaded media exceeded the provider's per-type size cap (5MB image / 16MB video or audio / 100MB document).


profile_limit_reached — 403

The workspace's active_profiles plan limit is already reached. Carries upgrade_required: true and upgrade: { plan } — the fix is adding capacity (a plan upgrade, or on Agency/Enterprise a support-granted override), not a different credential.


idempotency_key_conflict — 409

The Idempotency-Key header was already used for a different operation, or for the same operation with a different request body, than the one it first claimed. Not retryable as sent: generate a fresh key per logical write attempt, or resend the original body under the original key.


idempotency_key_in_flight — 409

The first request carrying this Idempotency-Key has not finished yet, so the retry was refused instead of risking a second delivery. Retry the same key with backoff: once the original finishes, the same key replays its response for 24 hours.


demo_purge_blocked — 409

A database trigger refused part of the delete because the rows are audit evidence. The demo profile is archived and its seeded data removed; the conversion-export and signal-audit rows stay attached to it. Retrying will not change this — nothing further is needed.


workspace_archived — 410

The invitation's workspace has been archived. Archiving is terminal — the workspace will never come back, so a new invitation to it would fail the same way. Not retryable; ask to be invited to a different, active workspace.


app_provisioned_workspace — 403

The workspace was provisioned by a consumer app (acquisition_source = 'oauth'), and Atribu sends its people no email at all — the app that owns the relationship delivers its own invitations and reports. No credential changes this.


audience_tos_required — 428

Meta has no record that this ad account accepted its Custom Audience terms, so it refuses to create the audience. Nothing about the request or the credential is wrong and no retry will help: the advertiser must accept the terms for that ad account in Meta Ads Manager, then confirm the proposal again.


customer_not_in_profile — 422

The named customer is not an active customer of this profile — either it belongs to a different profile, or it has been merged into another customer.


platform_safe_required — 409

The profile is not in Platform-Safe mode, so it has no use for a Clean Dataset — and creating one spends one of the ad account's self-created-pixel slots. Switch the profile first with PUT /api/v1/conversion-sync/catalog (privacy_mode: "platform_safe"), then provision.


meta_write_permission_missing — 403

The profile's Meta Ads connection was granted read-only access — its recorded grant does not include ads_management — so Atribu cannot pause, resume or re-budget ads through it. Nothing is wrong with your key and no retry helps: the advertiser must reconnect Meta Ads and accept the "manage advertisements" permission, then retry.


budget_change_out_of_bounds — 422

The requested daily budget moves more than ±50% of the ad set's live budget in one step, or falls below Atribu's floor of 100 minor units. Nothing was written to Meta (the refused attempt is still audited). Apply a smaller step now and another one later. Atribu does not read Meta's own per-account minimum, which is higher in most currencies (in CLP, about 1,000 pesos): a budget between the two is attempted and Meta's refusal arrives as validation_error with Meta's message.


clean_dataset_has_live_ad_sets — 409

Switching this Platform-Safe profile back to its existing Meta dataset would stop the ad sets that optimize on its Clean Dataset from receiving conversions, and some are still delivering (or Meta could not be read to check). Move or pause those ad sets first, or resend POST /api/v1/conversion-sync/platform-safe/target with confirm_live_ad_sets: true.


meta_connection_required — 409

This operation needs the profile's Meta Ads connection (its token and ad account), and none is usable: no enabled Meta CAPI destination names one, and the profile does not have exactly one connected Meta Ads account to fall back to. Connect Meta Ads through the connect rail (or create the Meta CAPI destination on the connection to use), then retry.


The automated duplicate-and-swap creates new ad sets (new ids, a Meta learning-phase reset, a possible window of double spend), so an API key or MCP user token must prove a person accepted the current consent text. The consent object was missing, named another version, hashed another text, or was accepted more than 24 hours ago. error.consent names the current version and text_hashes: render that text verbatim (see Partners → Automated duplicate-and-swap), have the person accept it, and re-send with consent: { version, text_hash, accepted_by, accepted_at }. Nothing was sent to Meta.


amount_invalid — 422

The refund amount is zero or negative, or exceeds the payment's value minus what the provider has already reversed for it.


manual_refund_exists — 422

This payment already has a manually recorded refund. Undo it (DELETE the refund) before recording a new amount — there is at most one manual refund per payment.


payment_assignment_incomplete — 502

The payer assignment was recorded but has not yet propagated to where attribution reads it. Retry the request.


payment_ref_invalid — 422

The payment reference built for this payment was malformed.


status_invalid — 422

The status query parameter must be all or unidentified.


range_invalid — 422

from and to must both be present, parseable timestamps, with from on or before to.

On this page

Envelope fieldsError codesunauthorized — 401forbidden — 403insufficient_scope — 403not_found — 404invalid_parameter — 400invalid_request — 400validation_error — 422invalid_content — 400invalid_date_range — 400entitlement_required — 409healthcare_scope_forbidden — 403invalid_state — 409rate_limit_exceeded — 429connection_not_ready — 409provider_error — 502account_suspended — 403auth_revoked — 403mailbox_disabled — 403quota_exceeded — 429recipient_rejected — 422service_unavailable — 503not_implemented — 501internal_error — 500calendar_scope_required — 403calendar_unsupported — 422provider_not_configured — 501invalid_window — 400media_expired — 410whatsapp_register_limit — 429whatsapp_payment_required — 402whatsapp_otp_relay_unavailable — 503audit_timeout — 504weak_password — 422compromised_password — 422current_password_required — 400current_password_invalid — 400plan_upgrade_required — 402whatsapp_service_window_expired — 422payload_too_large — 413profile_limit_reached — 403idempotency_key_conflict — 409idempotency_key_in_flight — 409demo_purge_blocked — 409workspace_archived — 410app_provisioned_workspace — 403audience_tos_required — 428customer_not_in_profile — 422platform_safe_required — 409meta_write_permission_missing — 403budget_change_out_of_bounds — 422clean_dataset_has_live_ad_sets — 409meta_connection_required — 409consent_required — 409amount_invalid — 422manual_refund_exists — 422payment_assignment_incomplete — 502payment_ref_invalid — 422status_invalid — 422range_invalid — 422