Guides
Errors
Every failure has the same shape, a stable machine-readable code, and a request id you can quote to support.
The envelope
Failures are wrapped in an error object. Branch on code — never on the message, which is written for humans and may be reworded at any time.
- code — a stable identifier. This is what your integration should switch on.
- message — human-readable, and not stable. For internal errors it is always generic: no exception text, no SQL and no stack trace ever crosses the boundary.
- retryable — whether retrying the identical request could plausibly succeed. Published so you do not have to guess.
- detail — machine-readable context, always an object. It is empty rather than absent when there is nothing to add.
- request_id — identifies this exact request in SMSend's logs. Present on every error, including 4xx.
Correlating with support
The same identifier is returned as a header on every response, successful or not. Log it. Quoting it turns a support conversation about a failure that happened yesterday into a single lookup.
What to retry
Only three errors are marked retryable: a request still in flight under the same idempotency key, a rate limit, and an internal error. Everything else will fail again in exactly the same way, and retrying it just spends your rate limit.
Payment failures use 402 rather than 422 precisely so that they are easy to separate from malformed requests. A client that retries a validation error is wasting requests; a client that retries an insufficient-funds error in a loop is doing something worse.
Every error code
The complete published vocabulary. New codes can be added, so handle an unrecognised one as a generic failure rather than crashing.
Why other people's resources are 404
Requesting a batch, message or webhook that belongs to another account returns 404, not 403 — the same answer as a resource that has never existed. A 403 would confirm that the identifier is real, which turns the endpoint into a way of testing whether particular ids exist.