Machine-readable specification
The complete OpenAPI 3.0.3 description of the Unsubscribe API — request and response schemas, parameters, error envelopes, and security definitions — is published alongside this guide. Generate client SDKs from it, or import it into Postman / Insomnia for interactive exploration.OpenAPI specification
Download
truagents.json — covers POST /oauth/token and all six /api/v1/unsubscribe/{email,sms,phone} operations.Summary
The Unsubscribe API lets a partner system maintain opt-out and opt-in rules keyed by raw identifier — an email address or a phone number. Each rule decides whether TruAgents may send on a given channel. Three properties distinguish these rules from per-contact suppression flags:- Decoupled from Contact records. Rules are stored separately from TruAgents contacts. You can push a rule for an email or phone that does not exist as a contact in any of your TruAgents organizations, and you can query state for any identifier whether or not a matching contact exists.
- Can apply globally across organizations. A single rule can govern sending behavior in any subset (or all) of the organizations you manage on TruAgents — including ones where the contact only exists in one of those organizations, or in none of them. This is configured per channel at onboarding time (see Channel scoping model below).
- Opt-ins are first-class. Rules carry an
unsubscribedboolean; setting it tofalsere-enables sending on that channel. TruAgents preserves the row and updatessourceandupdated_atto reflect the latest transition. A complete event log of every change is not part of this contract —sourceandupdated_atdescribe only the most recent transition.
Who this is for
External compliance, suppression-list, or CRM systems that need to keep TruAgents in sync with the partner’s source-of-truth opt-out / opt-in state.Rules, not contacts
Records pushed through this API are independent of any TruAgentsContact row:
- A rule is a
(channel, identifier, unsubscribed)triple stored against an admin-controlled channel scope, not tied to a TruAgents contact record. - TruAgents joins rules to contacts at send time, by matching the contact’s email or phone against rules in the contact’s organization’s channel scope. The rule existed before the contact was created, or after, or even if the contact is never created — the join is by raw identifier.
- A single rule can suppress (or re-enable) a channel for contacts living in multiple TruAgents organizations, when an administrator has configured those organizations to share a channel scope. The contact does not need to exist in every organization that the rule applies to — or in any of them.
- Deleting a contact in TruAgents does not delete the rule; pushing a rule does not create a contact.
- before any contacts exist in TruAgents (e.g., at onboarding, when pushing a known suppression list)
- alongside contact uploads (order of operations does not matter)
- for identifiers that may never become contacts (e.g., addresses you have suppressed on your side but never sent us as leads)
Channel scoping model
TruAgents tracks opt-out state for three channels: email, sms, and phone (voice calls). For each channel, a scope decides which TruAgents organizations a rule applies to. Each organization has an independent scope per channel, set by a TruAgents administrator at onboarding time and invisible to the partner over the API. Each channel’s scope works the same way:- Default: a fresh per-organization scope at organization creation. Rules pushed against an organization’s scope apply only to that organization.
- Shared scope: an administrator can point any group of organizations at the same scope value. Rules then apply across every organization in that group — including ones where no matching Contact exists.
phone (voice) channel has one additional configuration option: an administrator can set its scope equal to the same organization’s sms scope, producing a single suppression list that covers both SMS and voice on the same phone number. Equivalently, they can be kept distinct so SMS and voice can be opted out independently.
The partner does not pass a channel scope identifier. The partner may pass an org_slug to target a specific organization their API key is authorized to address (see Managing multiple organizations); when omitted, the request applies to the key’s default organization. The configured channel scoping then decides whether each write fans out to other organizations.
Examples
- An organization with the default per-organization SMS scope:
POSTan SMS opt-out for+15551234567— it suppresses SMS sends in that one organization only. - An organization whose SMS scope is shared with two sibling organizations (A, B, C):
POSTan SMS opt-out for+15551234567once — it suppresses SMS sends in A, B, and C, regardless of which of A, B, or C the phone exists as a contact in, including the case where the phone is not a contact in any of them. - An organization with the default per-organization email scope:
POSTan email opt-out forjohn@example.com— it suppresses email sends in that one organization only. - An organization whose email scope is shared with siblings:
POSTan email opt-out forjohn@example.com— it suppresses email sends across the entire shared scope, by the same rule as SMS. - An organization with
smsandphonescopes set to the same value: a singlePOSTto/api/v1/unsubscribe/smsand a separatePOSTto/api/v1/unsubscribe/phonefor the same phone number write to the same underlying list; setting one channel’sunsubscribedindependently of the other is not possible until an administrator splits the scopes.
Authentication
The Unsubscribe API uses TruAgents’ standard OAuth 2.0client_credentials flow. Your TruAgents administrator issues a client_id + client_secret pair; your service exchanges them for a short-lived bearer token at POST /oauth/token and sends the bearer token on every request below. See Authentication for the full flow, token lifetimes, refresh-token rotation, and error envelopes.
Each client_id is bound to a set of organizations with one designated as the default — requests that omit org_slug apply to the default; requests that include org_slug apply to that organization, provided it is in the authorized set. See Managing multiple organizations for the full mechanic and how to discover the slugs your key is authorized for.
Managing multiple organizations
Oneclient_id can address several TruAgents organizations on your behalf. Your TruAgents administrator picks the authorized set and the default when the credential is issued and reviews them on every change.
Targeting a specific organization. Supply an org_slug on GET (as a query parameter) or POST (as a top-level body field). One call addresses exactly one organization — responses are never mixed.
Omitting org_slug. The request applies to the key’s default organization. The response echoes the resolved org_slug so you can confirm which default was used.
Authorized vs. unknown slug. The server returns 403 unauthorized_organization whenever the supplied slug is not currently in your key’s authorized set — including the case where the slug does not exist at all. The response body echoes the slug back so you can spot a typo without the server confirming whether the slug exists:
org_slug values your key is authorized for and which one is the default. Keep these in your configuration alongside the client_id.
Scope-sharing interaction. The authorized set bounds the org_slug values you may target. The downstream effect of each write is still decided by admin-configured channel scopes — addressing org A may suppress sends in sibling organizations whose channel scope is shared with A, even if those siblings are not in your authorized set. The channel scoping model section describes that mechanic; the authorized set just decides which slug you may name, not which orgs are affected downstream.
Rate limits. Per client_id, not per client_id × org_slug — see Rate limits for ceilings and how to request a bump.
Endpoints
Three channel-scoped resources, each withGET (list current state) and POST (push changes):
Query opt-out state
EachGET returns a page of state-change records for one channel, in one organization, newest first. Treat the endpoint as a change stream: each call returns a page within the pagination window; cursor advances toward older records; since and until bound the window. The same parameters apply to all three endpoints — the examples below illustrate different combinations of org_slug, since, and limit.
GET /api/v1/unsubscribe/email (default organization, latest 50)
GET /api/v1/unsubscribe/sms (since a fixed timestamp)
GET /api/v1/unsubscribe/phone (explicit org_slug)
since and until are pagination bounds, not arbitrary filters: each call returns a page in updated_at descending order, and subsequent calls via cursor walk toward older records until has_more is false. To find the current state of a specific identifier, walk far enough — the most recent record for that identifier (the first one encountered) is its current state.
Response — email endpoint
org_slugechoes the organization this page belongs to — either the value you supplied, the one carried bycursor, or your key’s default organization when neither was provided.- Each record represents the current state of one identifier on this channel; the page only contains identifiers whose most recent transition falls inside the pagination window.
unsubscribed: true⇒ opted out (TruAgents will not send on this channel).unsubscribed: false⇒ opted in (record kept for history; sends proceed normally if no other suppression applies).sourcerecords who or what last changed the state. The same five values apply to every endpoint (email, SMS, voice):api— pushed by the partner via this API.admin— a TruAgents operator manually changed the state via TruAgents’ internal admin surface (e.g. compliance response, support-driven override). Theupdated_atreflects the operator’s action; the operator identity is not exposed in the partner response.import— bulk import: CSV upload, CRM sync (HubSpot / Salesforce / Mailchimp), or the one-time legacy backfill from pre-existing opt-out state.user_action— recipient took a concrete provider-observed action: SendGrid unsubscribe link, SendGrid list-unsubscribe header, Twilio STOP / UNSUBSCRIBE keyword. Opt-ins via Twilio START / YES / UNSTOP emit the same source withunsubscribed: false. On the voice endpoint,user_actionis visible when your administrator has unified your organization’s SMS and voice scopes and a Twilio event suppressed the shared row.user_intent— TruAgents inferred opt-out intent from an inbound message. The provider did not observe a subscription action; TruAgents acted on the recipient’s behalf. Only visible for organizations enrolled in TruAgents’ inbound-message classifier.
updated_atis the timestamp of the most recent state change.- Records are ordered by
updated_atdescending. next_cursorisnullwhen there are no more results.
Push opt-out / opt-in changes
Each endpoint accepts a batch of{ identifier, unsubscribed } items targeting one organization. Both opt-outs (true) and opt-ins (false) are accepted. Up to 10,000 items per request.
Items are processed in array order. If the same identifier appears more than once in the same batch, only the first occurrence is applied; every subsequent occurrence is rejected with reason: "duplicate identifier" and surfaces in skipped_items. Dedupe on your side before posting.
The examples below show explicit org_slug on each channel; omit it to use the key’s default organization.
POST /api/v1/unsubscribe/email
POST /api/v1/unsubscribe/sms
POST /api/v1/unsubscribe/phone
org_slug.
Request body (same shape for all three endpoints, identifier field differs)
Each item:
Response
org_slugechoes the organization the batch was applied to — either the value you supplied, or your key’s default organization when you omitted it.- The operation is not atomic — each item is processed independently. Partial failures do not roll back successful updates.
processedis the count of items accepted intoupdated(i.e.,items.length − skipped_items.length). The call is idempotent: items already at the requested state are still counted inprocessedand listed inupdated, even though no state change occurred in the database.updatedlists every accepted item — one input item produces exactly one entry. The returnedupdated_atis the row’s current state-change timestamp; to detect whether this write moved state, compare it against theupdated_atyou previously observed for the same identifier (from an earlier GET or POST response).skipped_itemslists items rejected due to validation conflicts (see below).
Conflicts and validation
Error responses
What TruAgents tracks per record
The channel is implicit in the endpoint you call. Each record persisted by the API or by an internal source carries:
Opt-ins (transitions from
true → false) are first-class — TruAgents preserves the record with unsubscribed: false, updates source and updated_at to reflect the new state, and respects the most recent change regardless of which source wrote it. Only the latest state is queryable; intermediate transitions are not retained.
Internal sources that can change state
In addition to the partner API, the same records are written by:- Provider-managed subscription actions — recipient-driven opt-outs (SendGrid unsubscribe link, SendGrid list-unsubscribe header, Twilio STOP keyword) and opt-ins (Twilio START / YES / UNSTOP, and equivalents). Source:
user_actionon both channels. If your TruAgents administrator has configured your SMS and voice scopes to share a single suppression list, Twilio-sourced records also surface on the voice (/phone) endpoint — they suppress both channels in that configuration. - Bulk imports — CSV uploads and CRM sync (HubSpot / Salesforce / Mailchimp). Source:
importon every endpoint. Imported opt-outs are pre-existing state whose original mechanism is not persisted on the row itself; the ingestion pipeline is identified separately on internal records for identifiable imports. - TruAgents admin UI — manual changes by a TruAgents operator. Source:
admin. Admin changes to the SMS or voice list under a unified scope behave the same as provider-sourced ones: the single underlying record is visible on both the/smsand/phoneendpoints. - TruAgents classifier — when TruAgents infers opt-out intent from an inbound message, TruAgents’ classifier writes
source='user_intent'. No partner or admin action is involved; TruAgents acts on the recipient’s behalf. This is distinct fromuser_action, where a provider (SendGrid / Twilio) directly observes the recipient’s subscription action.
Common mistakes
- Calling the wrong endpoint for the channel — SMS opt-outs go to
/api/v1/unsubscribe/sms, voice opt-outs go to/api/v1/unsubscribe/phone. They are independent lists. - Including the wrong identifier field in an item — the email endpoint accepts only
email; the SMS and voice endpoints accept onlyphone. - Mixing organizations in one batch —
org_slugis top-level; one request targets one organization. Build one batch per organization and send them in sequence or in parallel. - Sending a mismatched
org_slugalongsidecursoron aGET—org_slugis optional whencursoris supplied (the cursor already encodes the target). Sending a matching value as a sanity check is fine; sending a different value returns400. To switch organizations, drop the cursor and start a fresh page. - Sharing cursors between deployments or credentials — cursors encode the target scope and pagination position, not an identity. Two keys authorized for the same organization can in principle interoperate on a cursor, but treat cursors as ephemeral and non-transferable across credentials by convention. Each credential should walk its own pagination state; if a credential’s authorized set changes mid-walk, its own cursor may start returning
400 invalid_cursorand you re-anchor by dropping it. - Assuming a
403 unauthorized_organizationmeans the slug is wrong — it can also mean the slug exists but your administrator has not (yet) added it to your key’s authorized set. The response does not distinguish the two; ask your administrator if you expected access. - Assuming an email opt-out applies across all your TruAgents organizations — each organization’s email channel scope is independent unless your TruAgents administrator has explicitly configured a shared channel scope at onboarding. Targeting one organization with
org_slugonly changes which organization you address; it does not change downstream channel-scope sharing. - Using inconsistent phone formats — phones must be E.164 (
+15551234567), not(555) 123-4567or555-123-4567. - Treating every item in the POST
updatedlist as a state change. Compare the returnedupdated_atagainst a value you previously observed for the same identifier to distinguish actual transitions from idempotent no-ops. For an identifier you have not previously read, the row may already exist at the requested state because an internal source (user_action,import,admin, oruser_intent) wrote it first — issue a GET before the POST if definitive no-op detection matters. - Branching on old channel-specific source values. Prior to this contract revision, email opt-outs surfaced as
user_sendgridand SMS / voice opt-outs asuser_twilio. Both collapse touser_actionunder the current enum, andexternal_apicollapses toapi. Partner integrations that branch on the old values must accept the new tokens; during the transition window a partner may observe both old and new values in the same response until the underlying rows are updated.

