Skip to main content

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, the unsubscribe group discovery endpoint, and all write / list operations under /api/v1/unsubscribe/.
This guide is the canonical prose narrative; the OpenAPI document is the machine-readable contract. If you spot drift between the two, treat this page as authoritative and let us know.

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. Rules live in unsubscribe groups: named, shareable block lists with a client-visible id (ug_-prefixed). Every organization has a default group per channel; an administrator can point several organizations at one group so they share a single list. The discovery endpoint shows you every group your credential can reach and which organizations use it — the sharing topology is self-service, not out-of-band. 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 rule in a group shared by several organizations governs sending behavior in all of them — including ones where the contact exists in one of those organizations, or in none of them. See Unsubscribe groups below.
  • Opt-ins are first-class. The /add endpoints record opt-outs; the /remove endpoints record opt-ins. TruAgents preserves the row in either direction and updates source and updated_at to reflect the latest transition. A complete event log of every change is not part of this contract — source and updated_at describe 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 TruAgents Contact row:
  • A rule is an (identifier, unsubscribed) pair stored in an unsubscribe group, not tied to a TruAgents contact record.
  • TruAgents joins rules to contacts at send time, by matching the contact’s email or phone against the rules in the group each organization’s channel points at. 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 suppresses (or re-enables) a channel for contacts living in every organization that uses the rule’s group on that channel. The contact does not need to exist in every one of those organizations — or in any of them.
  • Deleting a contact in TruAgents does not delete the rule; pushing a rule does not create a contact.
This means you can populate the unsubscribe API in any order relative to contact uploads:
  • 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)

Unsubscribe groups

TruAgents tracks opt-out state for three channels: email, sms, and phone (voice calls). Rules live in unsubscribe groups — shareable block lists with a client-visible id (ug_-prefixed), a kind, and an owning organization:
  • A group’s kind is either email or phone_number. Email-channel requests must target an email-kind group; SMS and voice requests must target a phone_number-kind group. A mismatch returns 400.
  • Every organization has a default group per channel (email, sms, phone), created automatically with the organization. By default each organization’s groups are its own — rules apply to that organization only.
  • Sharing: an administrator can point any set of organizations’ channels at one group. Rules in that group then apply to every organization using it — including ones where no matching Contact exists.
  • Unified SMS + voice: when an administrator points an organization’s sms and phone channels at the same phone_number-kind group, a single rule covers both channels for that phone number. A recipient’s STOP text — or your /sms/add push — suppresses voice calls too. Keep the channels on distinct groups when they must be controllable independently. Per-channel granularity inside one shared group is intentionally impossible.
You can always see your reachable groups and the sharing topology via discovery.

Examples

  • Default setup: POST /api/v1/unsubscribe/sms/add for +15551234567 writes to your organization’s own SMS group — it suppresses SMS sends in that one organization only.
  • Shared group across organizations A, B, C: one /sms/add push 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.
  • Unified SMS + voice: with both channels on one group, /sms/add and /phone/add for the same phone number write to the same underlying list; opting out one channel independently of the other is not possible until an administrator splits the channels onto distinct groups.

Authentication

The Unsubscribe API uses TruAgents’ standard OAuth 2.0 client_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. A group is reachable with your credential when its owning organization is in your authorized set, or when any organization in your authorized set uses it on some channel.

Targeting a group

Every write and list request resolves to exactly one unsubscribe group. Three ways to pick it — use at most one: Both fields travel as top-level body fields on POST and as query parameters on GET. Supplying both group_id and org_slug in one request returns 400 bad_request. A pagination cursor already encodes the target group — supplying group_id or org_slug alongside a cursor also returns 400. Every response echoes the resolved group_id, so the untargeted forms still tell you exactly which list you touched. Authorized vs. unknown. The server returns 403 unauthorized_organization when the supplied org_slug — or group_id — is not reachable with your credential, including the case where it does not exist at all. The body echoes the offending value back so you can spot a typo without the server confirming whether it exists:
No default group configured. If the resolved organization has no group wired for the endpoint’s channel, the server returns 400 default_group_not_configured. Ask your TruAgents administrator to configure one, or target another list explicitly via group_id. Rate limits. Per client_id, not per client_id × organization — see Rate limits for ceilings and how to request a bump.

Endpoints

Discover your unsubscribe groups

GET /api/v1/unsubscribe-groups returns every group reachable with your credential, with per-group usage:
GET /api/v1/unsubscribe-groups
  • id is the group_id you pass when targeting the group.
  • kind tells you which endpoints accept the group — email for /email/*, phone_number for /sms/* and /phone/*.
  • unsubscribes counts identifiers currently opted out in the group (opted-back-in records are excluded).
  • owner is the owning organization’s slug — or null when the owner is outside your authorized set (you reach the group only because one of your organizations uses it).
  • used_by lists which of your authorized organizations use the group, per channel. Organizations outside your authorized set never appear, even when they share the group.
  • The response is not paginated; a credential’s reachable set is expected to fit one response.

Query opt-out state

Each GET /api/v1/unsubscribe/{channel} returns a page of state-change records from one group, 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.
GET /api/v1/unsubscribe/email (default organization's email group, latest 50)
GET /api/v1/unsubscribe/sms (explicit group, since a fixed timestamp)
GET /api/v1/unsubscribe/phone (explicit org_slug)
Query parameters 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
Response — SMS / Voice endpoints
  • group_id identifies the group this page belongs to — the one you targeted, or the one your org_slug / default organization resolved to.
  • Each record represents the current state of one identifier in this group; the page only contains identifiers whose most recent transition falls inside the pagination window.
  • unsubscribed: true ⇒ opted out (TruAgents will not send on the channels routed through this group).
  • unsubscribed: false ⇒ opted in (record kept for history; sends proceed normally if no other suppression applies).
  • source records 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). The updated_at reflects 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 with unsubscribed: false. On the voice endpoint, user_action is visible when your administrator has unified your organization’s SMS and voice channels on one group and a Twilio event suppressed the shared record.
    • 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_at is the timestamp of the most recent state change.
  • Records are ordered by updated_at descending.
  • next_cursor is null when there are no more results.

Push opt-out / opt-in changes

The URL verb decides the direction: /add opts identifiers out, /remove opts them back in. Each request carries a batch of items targeting one group. Up to 10,000 items per request. Items carry only the identifier — direction comes solely from the URL verb. Bodies that include a legacy unsubscribed field are rejected as an unknown field with 400 bad_request.
POST /api/v1/unsubscribe/email/add
POST /api/v1/unsubscribe/sms/add (explicit group)
POST /api/v1/unsubscribe/email/remove (opt back in)
To suppress both SMS and voice for the same phone under split groups, call both channels’ /add endpoints. Under a unified group one call suffices — see Unsubscribe groups. To write to multiple lists, send one request per target. Request body (same shape on every write endpoint; identifier field differs by channel) Each item: Response
  • group_id echoes the group the batch was applied to.
  • The batch is atomic — all or nothing. The whole request is validated before anything is written. Any invalid item rejects the entire request with 400 invalid_item and zero rows persisted; there is no partial success and no skipped-items list. Fix the reported item and resend the whole batch.
  • processed is the count of items written (always items.length on a 200). The call is idempotent: items already at the requested state are still counted and listed in updated, even though no state change occurred in the database.
  • updated lists every item — one input item produces exactly one entry. The returned updated_at is the row’s current state-change timestamp; to detect whether this write moved state, compare it against the updated_at you previously observed for the same identifier.

Conflicts and validation

An invalid item rejects the whole batch: 400 with error: "invalid_item", the zero-based item_index of the first invalid item, and an item_error reason:
Request-level rejections (no item_index):

Error responses

What TruAgents tracks per record

The channel is implicit in the endpoint you call; the group is echoed as group_id. Each record persisted by the API or by an internal source carries: Opt-ins 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_action. Under a unified SMS + voice group, Twilio-sourced records surface on both the /sms and /phone list endpoints — one record suppresses both channels.
  • Bulk imports — CSV uploads and CRM sync (HubSpot / Salesforce / Mailchimp). Source: import on 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 under a unified group behave the same as provider-sourced ones: the single underlying record is visible on both the /sms and /phone endpoints.
  • 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 from user_action, where a provider (SendGrid / Twilio) directly observes the recipient’s subscription action.
When two sources change state for the same identifier, the most recent write wins. The partner API and internal sources do not have a conflict resolution beyond timestamps; each push is authoritative for the moment it is applied.

Common mistakes

  • Posting to the bare POST /api/v1/unsubscribe/{channel} path — it is not a write endpoint (only GET lists there). The direction lives in the URL: /add to opt out, /remove to opt in.
  • Sending an unsubscribed field in a write item — direction lives in the URL verb only, so the field is not part of the request contract and is rejected as an unknown field with 400 bad_request.
  • Treating a 400 invalid_item as partial success — the batch is all-or-nothing. Nothing was written; fix the item at item_index (or dedupe / validate the batch client-side) and resend the whole request.
  • Targeting an email-kind group from /sms/* or /phone/* (or a phone_number-kind group from /email/*) — group kinds are per-channel; check kind in the discovery response.
  • Supplying both group_id and org_slug — pick one targeting form per request. Both present returns 400.
  • Sending group_id or org_slug alongside a cursor — the cursor already encodes the target group. To switch lists, drop the cursor and start a fresh page.
  • Reusing cursors minted before this contract revision — they no longer decode and return 400 invalid_cursor. Drop the cursor and re-anchor with a fresh request.
  • Calling the wrong endpoint for the channel — SMS opt-outs go to /api/v1/unsubscribe/sms/add, voice opt-outs go to /api/v1/unsubscribe/phone/add. Whether they share one list is decided by your administrator’s group configuration; check discovery.
  • Mixing targets in one batch — group_id / org_slug are top-level; one request targets one group. Build one batch per target and send them in sequence or in parallel.
  • Assuming a 403 unauthorized_organization means the value is wrong — it can also mean the organization or group exists but is not (yet) reachable with your key. The response does not distinguish the two; check discovery, then ask your administrator if you expected access.
  • Assuming an email opt-out applies across all your TruAgents organizations — organizations share a list only when an administrator points them at the same group. The discovery endpoint’s used_by shows the actual sharing topology.
  • Using inconsistent phone formats — phones must be E.164 (+15551234567), not (555) 123-4567 or 555-123-4567.
  • Treating every item in the updated list as a state change. Compare the returned updated_at against 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 record may already exist at the requested state because an internal source (user_action, import, admin, or user_intent) wrote it first — issue a GET before the POST if definitive no-op detection matters.
  • Branching on old channel-specific source values. Prior contract revisions surfaced user_sendgrid / user_twilio (now user_action) and external_api (now api). Partner integrations must branch on the current five values only.