Pager

Pager API

Version 1.0.0. The machine-readable contract is at /v1/openapi.json.

The Pager API sends and receives SMS, manages the numbers that carry it, and records the consent that makes sending it lawful.

Authentication

Every endpoint except /v1/health requires an API key, presented as a bearer token:

Authorization: Bearer pk_live_...

A key belongs to exactly one account and every request is scoped to it. A resource belonging to another account is reported as 404, not 403: answering 403 would confirm the resource exists and let one tenant enumerate another's message and number ids by watching the status code.

The key is shown once, when it is issued, and stored only as a hash. A key that is lost cannot be recovered, only replaced.

Errors

Every failure has the same shape, so a client can write one error path:

{ "error": { "type": "invalid_request", "message": "...", "details": [ ... ] } }

Match on type, which is stable. message is for humans and may change without notice. details names the offending fields when the failure is attributable to input.

recipient_opted_out is its own type rather than a bare 403 because a caller has to be able to tell "this person unsubscribed, remove them from your list" apart from "your key may not do that". Retrying the first will never succeed.

Every response carries X-Correlation-Id. Quoting it in a support report identifies the exact request in the platform logs.

Pagination

Collections are cursor-paginated, not offset-paginated: an offset silently skips or repeats records when the underlying set changes between pages, which it does constantly here.

Read data, and if hasMore is true, pass nextCursor back as cursor. nextCursor is null on the last page, so a client can loop on its presence rather than comparing cursors.

Idempotency

POST /v1/messages accepts an Idempotency-Key header. A repeat of the same key returns the original message with 200 instead of sending a second one and returning 202. The distinction tells a caller retrying after a timeout that its first attempt did land.

Keys are at most 128 characters of letters, digits and ., :, _, -. An over-long key is rejected rather than truncated, because truncating would silently merge two distinct requests into one message.

Rate limits

Two buckets are charged: one for requests generally, and a narrower one for sends. A send costs carrier capacity shared with every other tenant, which is scarcer than the database query behind a GET; one bucket for both would let a client polling its message list starve its own sending.

Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. A client that reads them never has to be refused at all. A 429 also carries Retry-After.

A send refused by validation or replayed from an idempotency key is refunded, so a client retrying safely is not punished for it.

Webhooks

Events are delivered to the account webhook, or to a per-number override, as a POST with two headers:

Pager-Timestamp: 1735689600
Pager-Signature: v1=<hex hmac-sha256>

The signature covers ${timestamp}.${rawBody} under the account's own signing secret, which GET /v1/account returns. Verify over the raw body before parsing it, compare in constant time, and reject a timestamp outside a five-minute window — without that check a captured request can be replayed forever.

Each account has its own secret. Verification is symmetric, so a shared secret would let any customer mint signatures for any other customer's endpoint.

Failed deliveries are retried on a backoff of 1s, 5s, 30s and 120s — five attempts in total. 5xx, 429, 408 and network errors are retried; every other 4xx is final, since a 404 at a mistyped path will still be a 404. Redirects are never followed. Every attempt is recorded and readable at GET /v1/webhook_deliveries.

Endpoints

Messages

Sending and reading SMS.

GET /v1/messages

List messages

Both directions, newest first. Filters combine, so direction=outbound&status=failed is everything that did not get through. An inbound message is stored delivered: it has already arrived, so there is no later outcome to wait for.

Parameters
NameInTypeNotes
statusqueryqueued | sent | delivered | failed | undelivered
directionqueryinbound | outbound
toquerystringAn E.164 phone number, including the leading plus.
fromquerystringAn E.164 phone number, including the leading plus.
cursorquerystringThe `nextCursor` from the previous page.
limitqueryintegerHow many records to return, from 1 to 100.
Responses
StatusDescription
200A page of messages.
401Standard error envelope.
422Standard error envelope.
429Standard error envelope.
500Standard error envelope.
Response body
FieldTypeNotes
id requiredstring
direction requiredinbound | outbound
from requiredstring
to requiredstring
body requiredstring
status requiredqueued | sent | delivered | failed | undelivered
encoding requiredGSM7 | UCS2 | nullNull on records that predate stored encoding.
segments requiredintegerHow many segments the carrier billed for.
errorCode requiredstring | null
errorMessage requiredstring | null
createdAt requiredstring
updatedAt requiredstring

POST /v1/messages

Send an SMS

Returns 202, not 200. The message is recorded and queued; the carrier has not seen it yet. Delivery is reported later through status and through webhooks.

The response carries the computed segment count and encoding, because carriers bill per segment and a single emoji can turn a one-segment message into three.

Sending to a number that has opted out fails with recipient_opted_out; that is final, and the number must be removed from the list rather than retried.

Parameters
NameInTypeNotes
Idempotency-KeyheaderstringRepeat a request safely. A replay returns the original message with `200` rather than sending again. At most 128 characters of letters, digits and `.`, `:`, `_`, `-`.
Request body
FieldTypeNotes
from requiredstringAn E.164 phone number, including the leading plus.
to requiredstringAn E.164 phone number, including the leading plus.
body requiredstringThe text to send.
Responses
StatusDescription
200A replay of an earlier request with the same `Idempotency-Key`. Nothing was sent this time.
202Accepted and queued.
401Standard error envelope.
403Standard error envelope.
422Standard error envelope.
429Standard error envelope.
500Standard error envelope.
Response body
FieldTypeNotes
id requiredstring
direction requiredinbound | outbound
from requiredstring
to requiredstring
body requiredstring
status requiredqueued | sent | delivered | failed | undelivered
encoding requiredGSM7 | UCS2 | nullNull on records that predate stored encoding.
segments requiredintegerHow many segments the carrier billed for.
errorCode requiredstring | null
errorMessage requiredstring | null
createdAt requiredstring
updatedAt requiredstring

GET /v1/messages/{id}

Fetch a message

Parameters
NameInTypeNotes
id requiredpathstringThe message id.
Responses
StatusDescription
200The message.
401Standard error envelope.
404Standard error envelope.
422Standard error envelope.
429Standard error envelope.
500Standard error envelope.
Response body
FieldTypeNotes
id requiredstring
direction requiredinbound | outbound
from requiredstring
to requiredstring
body requiredstring
status requiredqueued | sent | delivered | failed | undelivered
encoding requiredGSM7 | UCS2 | nullNull on records that predate stored encoding.
segments requiredintegerHow many segments the carrier billed for.
errorCode requiredstring | null
errorMessage requiredstring | null
createdAt requiredstring
updatedAt requiredstring

Numbers

The numbers an account can send from.

GET /v1/numbers

List numbers

The inventory this account may send from. Which carrier a number rides on is deliberately not reported: it is a routing decision the platform makes and changes.

Parameters
NameInTypeNotes
capabilityqueryvoice | sms
countryquerystringAn ISO 3166-1 alpha-2 country code.
cursorquerystringThe `nextCursor` from the previous page.
limitqueryintegerHow many records to return, from 1 to 100.
Responses
StatusDescription
200A page of numbers.
401Standard error envelope.
422Standard error envelope.
429Standard error envelope.
500Standard error envelope.
Response body
FieldTypeNotes
id requiredstring
phoneNumber requiredstring
capabilities requiredvoice | sms[]
country requiredstring | null
inboundEnabled requiredboolean
inboundWebhookUrl requiredstring | nullOverrides the account webhook for this number when set.
createdAt requiredstring
updatedAt requiredstring

GET /v1/numbers/{phoneNumber}

Fetch a number

Parameters
NameInTypeNotes
phoneNumber requiredpathstringThe number in E.164, url-encoded or not; `+12125551234` and `%2B12125551234` both work.
Responses
StatusDescription
200The number.
401Standard error envelope.
404Standard error envelope.
422Standard error envelope.
429Standard error envelope.
500Standard error envelope.
Response body
FieldTypeNotes
id requiredstring
phoneNumber requiredstring
capabilities requiredvoice | sms[]
country requiredstring | null
inboundEnabled requiredboolean
inboundWebhookUrl requiredstring | nullOverrides the account webhook for this number when set.
createdAt requiredstring
updatedAt requiredstring

PATCH /v1/numbers/{phoneNumber}

Update a number

Supply at least one of inboundEnabled or inboundWebhookUrl; an empty patch is rejected. Setting inboundWebhookUrl to null clears the override and returns the number to the account default, which is the only way a PATCH can express "remove this".

Parameters
NameInTypeNotes
phoneNumber requiredpathstringThe number in E.164.
Request body
FieldTypeNotes
inboundEnabledboolean
inboundWebhookUrlstring | null
Responses
StatusDescription
200The updated number.
401Standard error envelope.
404Standard error envelope.
422Standard error envelope.
429Standard error envelope.
500Standard error envelope.
Response body
FieldTypeNotes
id requiredstring
phoneNumber requiredstring
capabilities requiredvoice | sms[]
country requiredstring | null
inboundEnabled requiredboolean
inboundWebhookUrl requiredstring | nullOverrides the account webhook for this number when set.
createdAt requiredstring
updatedAt requiredstring

Consent

Who may be messaged, and the evidence for it. Carriers require this to be answerable on demand.

GET /v1/consent

List consent records

The current state for every number this account knows.

Parameters
NameInTypeNotes
statequeryopted_in | opted_out
cursorquerystringThe `nextCursor` from the previous page.
limitqueryintegerHow many records to return, from 1 to 100.
Responses
StatusDescription
200A page of consent records.
401Standard error envelope.
422Standard error envelope.
429Standard error envelope.
500Standard error envelope.
Response body
FieldTypeNotes
phoneNumber requiredstring
state requiredopted_in | opted_out
source requiredkeyword | web | api | operator
keyword requiredstring | null
createdAt requiredstring
updatedAt requiredstring

POST /v1/consent

Record consent

Returns 200, not 201: there is one record per number and this either created or replaced it, so a caller cannot read 201 as "was not already known".

Supply ipAddress and consentText for an opt-in. An opt-out never needs justifying, but an opt-in without evidence is an assertion with nothing behind it — which is what a carrier will ask you to produce.

Consent recorded here never overrides a STOP received by keyword; a handset always wins.

Request body
FieldTypeNotes
phoneNumber requiredstringAn E.164 phone number, including the leading plus.
state requiredopted_in | opted_out
ipAddressstring
consentTextstring
Responses
StatusDescription
200The consent record.
401Standard error envelope.
422Standard error envelope.
429Standard error envelope.
500Standard error envelope.
Response body
FieldTypeNotes
phoneNumber requiredstring
state requiredopted_in | opted_out
source requiredkeyword | web | api | operator
keyword requiredstring | null
createdAt requiredstring
updatedAt requiredstring

GET /v1/consent/{phoneNumber}

Consent history for a number

Every change, newest first, with what caused it. This is the answer to a carrier asking why a number was messaged.

Parameters
NameInTypeNotes
phoneNumber requiredpathstringThe number in E.164.
cursorquerystringThe `nextCursor` from the previous page.
limitqueryintegerHow many records to return, from 1 to 200.
Responses
StatusDescription
200The number's current consent state and a page of the events that produced it.
401Standard error envelope.
422Standard error envelope.
429Standard error envelope.
500Standard error envelope.
Response body
FieldTypeNotes
id requiredstring
phoneNumber requiredstring
state requiredopted_in | opted_out
source requiredkeyword | web | api | operator
keyword requiredstring | null
ipAddress requiredstring | null
consentText requiredstring | null
messageId requiredstring | null
occurredAt requiredstring

Webhooks

Delivery of events, and its audit.

GET /v1/webhook_deliveries

List webhook deliveries

What was sent to your endpoint, and what it answered. Each record carries the full attempt log, because the question this answers is "why did my endpoint not get this", and the answer is always in the attempts.

Parameters
NameInTypeNotes
statusquerypending | succeeded | failed
eventTypequerystring
cursorquerystringThe `nextCursor` from the previous page.
limitqueryintegerHow many records to return, from 1 to 100.
Responses
StatusDescription
200A page of deliveries.
401Standard error envelope.
422Standard error envelope.
429Standard error envelope.
500Standard error envelope.
Response body
FieldTypeNotes
id requiredstring
eventId requiredstring
eventType requiredstring
url requiredstring
status requiredpending | succeeded | failed
attempts requiredinteger
responseStatus requiredinteger | null
error requiredstring | null
deliveredAt requiredstring | null
createdAt requiredstring
updatedAt requiredstring
attemptLog requiredobject[]

Account

The account and its API keys.

GET /v1/account

The account this key belongs to

The cheapest way to confirm a key works and to discover which account it acts as, which matters when an operator holds keys for several tenants. Also where a customer collects the secret their webhooks are signed with.

Responses
StatusDescription
200The account.
401Standard error envelope.
422Standard error envelope.
429Standard error envelope.
500Standard error envelope.
Response body
FieldTypeNotes
id requiredstring
name requiredstring
status requiredactive | suspended
webhookUrl requiredstring | null
webhookSigningSecret requiredstring | nullThe secret outbound webhooks are signed with. Returned to the owning account and to nobody else.
createdAt requiredstring

GET /v1/api-keys

List API keys

Every key ever issued to this account, including revoked ones, so an audit can account for all of them. Secrets are never returned; only a hint of the last few characters.

Responses
StatusDescription
200The keys.
401Standard error envelope.
422Standard error envelope.
429Standard error envelope.
500Standard error envelope.
Response body
FieldTypeNotes
id requiredstring
name requiredstring
prefix requiredstringThe fixed prefix every Pager key carries.
hint requiredstringThe last few characters, to tell keys apart.
createdAt requiredstring
lastUsedAt requiredstring | null
revokedAt requiredstring | null

POST /v1/api-keys

Issue an API key

The secret is returned exactly once, in this response. It is stored only as a hash, so a caller that loses it must issue another key.

Request body
FieldTypeNotes
name requiredstringHow the key will be recognised.
Responses
StatusDescription
201The key, including its secret.
401Standard error envelope.
422Standard error envelope.
429Standard error envelope.
500Standard error envelope.
Response body
FieldTypeNotes
id requiredstring
name requiredstring
prefix requiredstringThe fixed prefix every Pager key carries.
hint requiredstringThe last few characters, to tell keys apart.
createdAt requiredstring
lastUsedAt requiredstring | null
revokedAt requiredstring | null
secret requiredstringThe key itself. Returned exactly once, at creation. It is not stored in recoverable form, so a caller that loses it must issue another.

DELETE /v1/api-keys/{id}

Revoke an API key

Takes effect immediately. Revoking is not deleting: the record stays so an audit can still see the key existed and when it stopped working.

Parameters
NameInTypeNotes
id requiredpathstringThe key id.
Responses
StatusDescription
200The revoked key.
401Standard error envelope.
404Standard error envelope.
422Standard error envelope.
429Standard error envelope.
500Standard error envelope.
Response body
FieldTypeNotes
id requiredstring
name requiredstring
prefix requiredstringThe fixed prefix every Pager key carries.
hint requiredstringThe last few characters, to tell keys apart.
createdAt requiredstring
lastUsedAt requiredstring | null
revokedAt requiredstring | null

System

Liveness.

GET /v1/health

Liveness probe

Reports only that the process is serving requests. Requires no authentication, so it can be used by a load balancer.

Responses
StatusDescription
200The service is up.
Response body
FieldTypeNotes
status requiredstring
service requiredstring
uptimeSeconds requiredinteger
version requiredstring