v1 · API reference

SMS

Beta. Send transactional and marketing SMS, and track each message's delivery lifecycle. Marketing sends require recipient consent and honor recipient-local quiet hours. Returns `not_found` on workspaces where the channel isn't enabled yet.

Base URL: https://www.unitpost.com/api/v1

SMS

Record SMS consent

POST/api/v1/contacts/{id}/sms-consent

POST /contacts/{id}/sms-consent
curl -X POST 'https://www.unitpost.com/api/v1/contacts/123/sms-consent' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'User-Agent: my-app/1.0' \
  -H 'Content-Type: application/json' \
  -d '{ }'
Response · 201
{
  "object": "sms_consent",
  "phone": "string",
  "recorded": {
    "id": "id_123",
    "action": "string",
    "source": "string",
    "occurred_at": "2026-01-01T00:00:00.000Z"
  }
}

Record an SMS opt-in or opt-out that a recipient gave you — typically a checkbox on your own signup form. This is the supported way to bring consent you already hold into Unitpost so marketing SMS is accepted for that contact. source is required: consent without recorded provenance is not consent, and the source is what makes the audit trail defensible. occurred_at may be backdated to when the recipient actually agreed (do not pretend historical consent happened just now); a future timestamp is rejected. Every call appends an immutable event, so the history is never overwritten. Recording an opt-out suppresses SMS for this contact only — it does not touch their email subscription. Requires the contacts:write capability.

Parameters1 field
FieldTypeDescription
idrequiredstring · path
Request body7 fields
FieldTypeDescription
actionrequiredenumopt_in | opt_out
sourcerequiredenumweb_form | hosted_form | keyword | qr | verbal | paper | api_import | api
purposeenummarketing | transactional
phonestring
occurred_atstring
evidence_urlstring
disclosure_versionstring
Response · 201 fields3 fields
FieldTypeDescription
objectrequiredstring
phonerequiredstring
recordedrequiredobject
201401404422

List SMS messages

GET/api/v1/sms

GET /sms
curl -X GET 'https://www.unitpost.com/api/v1/sms' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'User-Agent: my-app/1.0'
Response · 200
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "sms",
      "id": "id_123",
      "to": "customer@example.com",
      "from": "you@yourdomain.com",
      "direction": "outbound",
      "status": "queued",
      "type": "transactional",
      "body": "string",
      "segments": 0,
      "country": "string",
      "scheduled_at": "2026-01-01T00:00:00.000Z",
      "last_error": "string",
      "error": {
        "code": "out_of_reach",
        "message": "string"
      },
      "created_at": "2026-01-01T00:00:00.000Z"
    }
  ]
}

Retrieve your workspace's SMS messages (outbound and inbound), newest-first and cursor-paginated. Use a message's id with Retrieve an SMS message to read its delivery state. Requires the sms:read capability.

Parameters3 fields
FieldTypeDescription
limitinteger · queryMax rows to return (default 20, max 100).
afterstring · queryReturn rows after this id (forward paging).
beforestring · queryReturn rows before this id (backward paging).
Response · 200 fields14 fields
FieldTypeDescription
objectrequiredstring
idrequiredstring
torequiredstring
fromrequiredobject
directionrequiredenumoutbound | inbound
statusrequiredenumqueued | scheduled | sending | sent | delivered | failed | suppressed | canceled | received
typerequiredenumtransactional | marketing
bodyrequiredobject
segmentsrequiredinteger
countryrequiredobject
scheduled_atrequiredobject
last_errorrequiredobjectLegacy free-text error. Prefer `error.code`.
errorrequiredobjectSet on `suppressed` messages: the stable skip reason and its sentence. Null otherwise.
created_atrequiredstring
200401403422

Send an SMS message

POST/api/v1/sms

POST /sms
curl -X POST 'https://www.unitpost.com/api/v1/sms' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'User-Agent: my-app/1.0' \
  -H 'Content-Type: application/json' \
  -d '{ }'
Response · 200
{
  "object": "sms",
  "id": "id_123",
  "to": "customer@example.com",
  "from": "you@yourdomain.com",
  "direction": "outbound",
  "status": "queued",
  "type": "transactional",
  "body": "string",
  "segments": 0,
  "country": "string",
  "scheduled_at": "2026-01-01T00:00:00.000Z",
  "last_error": "string",
  "error": {
    "code": "out_of_reach",
    "message": "string"
  },
  "created_at": "2026-01-01T00:00:00.000Z"
}

Send a single SMS — now, or at a future time by passing scheduled_at. The recipient must be a valid E.164 number; marketing sends additionally require the recipient's prior express consent on record and honor recipient-local quiet hours (they are deferred to the next allowed window, never dropped). The sender is chosen by the recipient's country — the active number or Sender ID whose reach covers it, exactly as campaigns route — unless you pin one with from (a snum_… id from List SMS numbers, or the number / Sender ID string). Pass an Idempotency-Key header to make retries safe: the same key with the same body replays the original message instead of sending (and billing) twice. Billing is per segment. Read delivery state back with Retrieve an SMS message. Requires the sms:send capability.

Request body6 fields
FieldTypeDescription
torequiredstringRecipient phone number in E.164 format, e.g. `+15551234567`.
fromstringOptional sender to send from: a number id from `GET /sms/numbers` (`snum_…`), or the E.164 number / Sender ID string itself (e.g. `+18005550100`, `Acme`). Must belong to your workspace, be active, and be able to reach the recipient's country — otherwise `404 not_found`, `403 sender_not_approved`, or `422 sender_cannot_reach`. Omit to route automatically: Unitpost picks the active sender whose reach covers the recipient's country (10DLC before toll-free for US recipients; the matching Sender ID elsewhere), exactly as campaigns do.
bodyrequiredstringMessage text. Billing is per segment (160 GSM-7 chars single-segment, 153 per segment concatenated; 70/67 for Unicode).
typeenumtransactional | marketingMarketing sends require the recipient's prior express consent on record and honor recipient-local quiet hours.
scheduled_atstringISO-8601 time to send at. Omit to send immediately.
create_contactbooleanCreate a contact for this recipient if one doesn't exist yet. Defaults to false — sends don't modify your contact list. When the recipient is already a contact, the message is attached to them either way. A contact created this way records no consent, so it can't be used for marketing sends.
Response · 200 fields14 fields
FieldTypeDescription
objectrequiredstring
idrequiredstring
torequiredstring
fromrequiredobject
directionrequiredenumoutbound | inbound
statusrequiredenumqueued | scheduled | sending | sent | delivered | failed | suppressed | canceled | received
typerequiredenumtransactional | marketing
bodyrequiredobject
segmentsrequiredinteger
countryrequiredobject
scheduled_atrequiredobject
last_errorrequiredobjectLegacy free-text error. Prefer `error.code`.
errorrequiredobjectSet on `suppressed` messages: the stable skip reason and its sentence. Null otherwise.
created_atrequiredstring
200401403404409422429

List SMS brands

GET/api/v1/sms/brands

GET /sms/brands
curl -X GET 'https://www.unitpost.com/api/v1/sms/brands' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'User-Agent: my-app/1.0'
Response · 200
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "sms_brand",
      "id": "id_123",
      "name": "Example",
      "legal_name": "string",
      "website": "string",
      "domain": "string",
      "contact_email": "string",
      "status": "incomplete",
      "missing": [
        "string"
      ],
      "ten_dlc_verified": false,
      "tax_id_on_file": false,
      "number_count": 1,
      "editable": false,
      "created_at": "2026-01-01T00:00:00.000Z"
    }
  ]
}

Retrieve your SMS brands and their setup status. A brand is the business identity carriers register before they will approve a phone number, so this is the endpoint to poll when you need to know whether SMS is usable yet: status reports whether you or the carrier is the blocker, and missing lists exactly what is still required. Creating or editing a brand is a paid carrier submission and is only available in the dashboard. Requires the sms:read capability.

Parameters3 fields
FieldTypeDescription
limitinteger · queryMax rows to return (default 20, max 100).
afterstring · queryReturn rows after this id (forward paging).
beforestring · queryReturn rows before this id (backward paging).
Response · 200 fields14 fields
FieldTypeDescription
objectrequiredstring
idrequiredstringPrefixed brand id, e.g. `sbrand_…`.
namerequiredstringPublic-facing brand name.
legal_namerequiredstringRegistered legal entity name.
websiterequiredobjectBrand website URL.
domainrequiredobjectRegistrable domain derived from the website. The brand contact email must be on this domain.
contact_emailrequiredobject
statusrequiredenumincomplete | ready | pending | action_required | verified | rejected | archived`incomplete` and `action_required` need the customer to act in the dashboard; `pending` is waiting on the carrier.
missingrequiredstring[]Human-readable list of what is still required before this brand can back a number. Empty when nothing is missing.
ten_dlc_verifiedrequiredbooleanTrue when the 10DLC brand registration is approved.
tax_id_on_filerequiredboolean
number_countrequiredintegerActive (non-released) phone numbers backed by this brand.
editablerequiredbooleanFalse once the brand is locked (carrier-approved or in review). Locked brands can only be corrected when the carrier asks.
created_atrequiredstring
200401403404422

List SMS phone numbers

GET/api/v1/sms/numbers

GET /sms/numbers
curl -X GET 'https://www.unitpost.com/api/v1/sms/numbers' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'User-Agent: my-app/1.0'
Response · 200
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "sms_number",
      "id": "id_123",
      "phone_number": "string",
      "type": "toll_free",
      "status": "pending",
      "is_default": false,
      "country": "string",
      "reach": [
        "string"
      ],
      "brand_id": "brand_123",
      "use_case": {
        "name": "Example",
        "category": "string"
      },
      "action_required_reason": "string",
      "sender_id_group": {
        "name": "Example",
        "brand_id": "brand_123",
        "countries": [
          {
            "country": "string",
            "status": "pending"
          }
        ]
      },
      "created_at": "2026-01-01T00:00:00.000Z"
    }
  ]
}

Retrieve your SMS sending identities: phone numbers and alphanumeric Sender IDs. Only entries with status: "active" can send; a ten_dlc number has no phone_number until carriers approve the registration, a long_code is a local number issued outside the US (Canada today: country: "CA", reach: ["CA"], no carrier registration, use_case: null), and a simulator number only delivers to verified test destinations. use_case is what carriers approved the number to send. A sender_id entry is one country of a Sender ID: the identity is ONE string per brand enabled per country, the list returns one entry per (Sender ID, country), and every entry carries sender_id_group with the whole country set so you can group them. Requesting or releasing an identity is only available in the dashboard. Requires the sms:read capability.

Parameters3 fields
FieldTypeDescription
limitinteger · queryMax rows to return (default 20, max 100).
afterstring · queryReturn rows after this id (forward paging).
beforestring · queryReturn rows before this id (backward paging).
Response · 200 fields13 fields
FieldTypeDescription
objectrequiredstring
idrequiredstringPrefixed number id, e.g. `snum_…`.
phone_numberrequiredobjectE.164 number. Null while a 10DLC registration is still in review — the number is assigned on approval.
typerequiredenumtoll_free | ten_dlc | long_code | sender_id | simulator`long_code` is a local number issued outside the US (today: Canada, `country: "CA"`, reach `["CA"]`; instant, no carrier registration). `sender_id` is an alphanumeric Sender ID (one entry per enabled country; see `sender_id_group`). `simulator` only delivers to verified test destinations; it is not a production sender.
statusrequiredenumpending | action_required | active | rejected | releasedOnly `active` numbers can send.
is_defaultrequiredboolean
countryrequiredobjectISO-3166 alpha-2 country the number was issued in (`US` for toll-free and 10DLC, `CA` for a Canadian long code). Null until recorded.
reachrequiredstring[]ISO-3166 alpha-2 countries this number can deliver to. US toll-free and 10DLC numbers reach `["US"]` only; a Canadian long code reaches `["CA"]` only. A recipient outside this list is skipped before send (status `suppressed`, not billed) rather than forwarded best-effort from a shared identity. Empty while the country is unknown.
brand_idrequiredobjectThe brand this number is registered under.
use_caserequiredobjectWhat carriers approved this number to send. Required for approval; submitted with the number.
action_required_reasonrequiredobjectSet when status is `action_required` or `rejected`.
sender_id_grouprequiredobjectPresent on `sender_id` entries only. A Sender ID is ONE identity per brand enabled per country: the list still returns one entry per (Sender ID, country) so existing integrations keep working, and every entry of the same identity carries the same `sender_id_group` describing the whole set.
created_atrequiredstring
200401403404422

Retrieve an SMS message

GET/api/v1/sms/{id}

GET /sms/{id}
curl -X GET 'https://www.unitpost.com/api/v1/sms/123' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'User-Agent: my-app/1.0'
Response · 200
{
  "object": "sms",
  "id": "id_123",
  "to": "customer@example.com",
  "from": "you@yourdomain.com",
  "direction": "outbound",
  "status": "queued",
  "type": "transactional",
  "body": "string",
  "segments": 0,
  "country": "string",
  "scheduled_at": "2026-01-01T00:00:00.000Z",
  "last_error": "string",
  "error": {
    "code": "out_of_reach",
    "message": "string"
  },
  "created_at": "2026-01-01T00:00:00.000Z"
}

Retrieve one SMS message's current status, delivery state, and segment count by id. Requires the sms:read capability.

Parameters1 field
FieldTypeDescription
idrequiredstring · pathThe SMS message id (`sms_…`).
Response · 200 fields14 fields
FieldTypeDescription
objectrequiredstring
idrequiredstring
torequiredstring
fromrequiredobject
directionrequiredenumoutbound | inbound
statusrequiredenumqueued | scheduled | sending | sent | delivered | failed | suppressed | canceled | received
typerequiredenumtransactional | marketing
bodyrequiredobject
segmentsrequiredinteger
countryrequiredobject
scheduled_atrequiredobject
last_errorrequiredobjectLegacy free-text error. Prefer `error.code`.
errorrequiredobjectSet on `suppressed` messages: the stable skip reason and its sentence. Null otherwise.
created_atrequiredstring
200401403404