Errors and limits

The response envelope, every error code, rate limits, pagination and data formats.

Everything here applies to every route under https://www.suzko.com/api/v1. Credentials are covered in API credentials.

The envelope

Every response is JSON in one of two shapes.

Success:

JSON
{
  "success": true,
  "data": { "object": "message", "id": "msg_Q4f8Zk2LmP7a" },
  "meta": { "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTEw..." }
}

meta is only there on lists.

Failure:

JSON
{
  "success": false,
  "error": "This recipient has opted out of texts from your business.",
  "code": "opted_out",
  "requestId": "req_7Hq2Lm9Xc4Vb8Nz1Kd5Rt3Wy"
}
  • Branch on code, never on error. code is stable; error is a human-readable message and can change.
  • requestId is req_ plus 24 letters and digits. It's also sent as the X-Request-Id header on every response, successes included. Quote it when you contact support.
  • Responses are never cached (Cache-Control: no-store).
  • Write requests reject fields the route doesn't know (400 invalid_request) instead of ignoring them, so a typo never silently drops an option.

Status codes

StatusMeans
200, 201, 202Done. 202 means accepted and queued (a send) or scheduled (a webhook test, verification or replay).
400The request is malformed: bad JSON, a missing or wrong field, an unknown field, a bad query parameter.
401The credential is missing, malformed, unknown, revoked, expired or suspended. One message for all of them.
402Money: texting is paused for an unpaid invoice, there's no payment method, or the spend limit is reached.
403Scope, permission, plan or account policy says no.
404Not found, or not yours. The API never says which.
409Conflict: an Idempotency-Key reused for a different request, or a webhook endpoint in the wrong state.
410A retired route.
413The request body is over 5 MB.
422Valid request, but it can't be carried out: the number, the recipient or the sender is unusable.
429Rate limited. Wait Retry-After seconds.
500Something broke on SUZKO's side.
503The API (or, for writes, the write path) is temporarily unavailable. Retry with backoff.

Error codes

CodeStatusMeaning
invalid_request400Malformed request (see above), a bad limit or cursor, or an Idempotency-Key that isn't 1–255 characters. Also 413 for a body over 5 MB, and 422 when from isn't one of your numbers (or is outside the credential's number list).
unauthorized401Missing, malformed, unknown, revoked, expired or suspended credential. Send Authorization: Bearer szk_pat_… or Bearer szk_mk_….
step_up_required401Developer console only: confirm it's you with a code from your authenticator app, then retry. The /api/v1 routes never return it.
payment_required402Texting is paused until your latest texting invoice is paid.
payment_method_required402This text would go past your included texts and there's no saved payment method.
spend_limit_reached402This text would take extra-text charges past your monthly spend limit.
forbidden_scope403The credential doesn't have the scope this route needs.
scope_not_permitted403A personal token whose holder no longer has the account permission the scope needs.
owner_only403Only the account owner can do this: adopt an endpoint, or manage service accounts and machine keys (developer console).
plan_required403The account has no active SUZKO Phone plan. Every texting route needs one, and so does creating a webhook endpoint.
account_suspended403The SUZKO account is closed, or SUZKO Phone access on it is suspended. Contact support.
not_found404No such message, endpoint or delivery for this credential. Also returned when the account has no SUZKO Phone account yet (send, batch, usage).
idempotency_mismatch409The Idempotency-Key was used for a different request, or an earlier request with it never finished. Use a new key.
endpoint_limit409The account already has 10 webhook endpoints.
endpoint_disabled409A test event was sent to an endpoint that's disabled (after repeated failed deliveries, or by SUZKO support).
endpoint_paused409The endpoint is paused because its creator lost access. error says why.
endpoint_not_active409A test event or replay to an endpoint that isn't verified and active yet (pending verification or paused). Verify it first.
not_replayable409That delivery can't be replayed: it isn't finished, it's a verification, or the creator isn't allowed the event any more.
endpoint_locked409SUZKO support disabled this endpoint, so it can't be deleted, rotated, re-verified or adopted. Contact support to restore it.
conflict409The endpoint changed while your request was in flight (another change landed first). Reload it and try again.
gone410A retired route. No current route returns it.
invalid_number422to isn't a valid, textable phone number.
not_registered422The sending number isn't registered for texting yet, or there's no number this credential can text from.
opted_out422The recipient opted out of your texts.
destination_blocked422The destination can't be texted from your account (premium-rate numbers; international unless support enabled it).
rate_limited429Over a rate limit, too many failed authentications from your address, too many webhook test events, or a request with the same Idempotency-Key is still running. Wait Retry-After seconds.
daily_limit_reached429Today's texting cap is used up (UTC day). Retry-After is the seconds until UTC midnight.
internal_error500Something went wrong on SUZKO's side. On a send, check before retrying (see Idempotency).
unavailable503The API or its write path is switched off for a moment, or a dependency is down. Reads keep working when only writes are paused.

Inside a batch, each rejected message carries one of the send codes in its own result instead of failing the whole request. See Send a batch.

Rate limits

Every request that authenticates counts against its credential's budget for its class, even one then refused for a missing scope or plan. A request that's also allowed counts against the whole account's budget (all its credentials together). Windows are fixed one-minute windows.

ClassRoutesPer credentialPer account
ReadsEvery GET120 per minute600 per minute
WritesPOST and DELETE under /webhooks20 per minute100 per minute
SendingPOST /messages, POST /messages/batch300 per minute600 per minute
  • A batch counts once, however many messages it holds.
  • The account budget is only charged when the credential's budget admits the request, so one runaway credential can't use up its siblings' share.
  • Idempotent replays count. Requests refused for a bad credential don't count against either budget; requests refused for a missing scope or plan count against the credential's budget only.
  • Over a budget: 429 rate_limited with Retry-After.

Every response that got as far as the rate limiter carries:

HeaderValue
X-RateLimit-LimitThe limit of the budget closest to running out (credential or account)
X-RateLimit-RemainingRequests left in that budget this window
X-RateLimit-ResetWhen that window resets, in Unix seconds
Retry-AfterOn a 429 only: seconds to wait

Failed authentication has its own limit: 30 failures per minute per IP address (per /64 for IPv6). Past that, failed attempts from that address get 429 rate_limited instead of 401. A valid credential is never refused because of it.

Other limits, separate from the request budgets:

  • Texting: the daily cap (daily_limit_reached) and your spend limit apply to every text, however it's sent. Accepted texts go out at the pace carriers allow; see Pacing.
  • Webhook test events: 5 a minute per endpoint, 30 an hour per account.
  • Request bodies: 5 MB (413). A full 1,000-message batch is far smaller.

Pagination

Lists return an array in data and the next page's cursor in meta.nextCursor. GET /messages and GET /numbers page:

  • limit: 25 by default, 100 at most. Anything else is 400 invalid_request.
  • cursor: pass meta.nextCursor from the previous page, exactly as you got it. It's opaque; an edited one is 400 invalid_request.
  • nextCursor is null on the last page.
  • Keep the same filters while you page.

GET /webhooks returns all of the credential's endpoints at once (an account has at most 10), so its nextCursor is always null.

Shell
curl "https://www.suzko.com/api/v1/messages?limit=100&cursor=eyJjcmVhdGVkQXQiOiIyMDI2LTEw..." \
  -H "Authorization: Bearer szk_mk_YOUR_MACHINE_KEY"

Data formats

  • Timestamps are RFC 3339 in UTC, with milliseconds: 2026-10-20T15:04:05.000Z.

  • Money is an exact decimal string plus an ISO 4217 currency, never a float:

    JSON
    { "amount": "50.00", "currency": "USD" }
    
  • Phone numbers are E.164: +13175550123.

  • Ids are strings with a type prefix, and case-sensitive: messages msg_…, phone numbers pn_…, webhook endpoints wh_…, deliveries whd_…, events evt_…, signing keys whk_…, requests req_…. The API never exposes internal database ids.

  • Every resource has an object field naming its type (message, phone_number, usage, webhook_endpoint, webhook_delivery, webhook_secret, event). Event payloads use contact and endpoint_verification too.