TeronaPay

Errors

Every non-2xx response uses the same envelope:

{
  "error": {
    "type": "invalid_request",
    "message": "amount must be positive"
  }
}
FieldTypeNotes
typestringStable machine-readable identifier. Branch on this
messagestringHuman-readable; safe to surface to your end users for invalid_request only

Status codes

CodeWhen
400invalid_request — bad body / missing field / wrong type
401unauthorized — missing or wrong API key
403forbidden — merchant suspended, role insufficient
404not_found — id doesn't exist or isn't yours
409conflict — uniqueness violation (e.g. duplicate slug)
422precondition_failed — request well-formed but resource state forbids it (refund non-succeeded payment, promote with no rotation in flight)
429rate_limited — over the per-merchant or per-route bucket
500internal — our fault; opaque message
502bad_gateway — downstream channel (Daraja, Stripe) rejected synchronously

Type catalog

invalid_request

Generic shape error. Usually fixable by changing the request body.

Common causes:

  • Missing required field (amount, reference, ...)
  • Wrong currency for the channel (e.g. currency: "USD" with channel: "mpesa_stk_push")
  • Malformed phone number (must be E.164 with leading +)
  • Non-positive amount

unauthorized

API key missing, malformed, or doesn't exist. Check Authorization header.

For /v1/auth/login and /v1/auth/register: bad credentials. We don't distinguish "user doesn't exist" from "wrong password" — both return unauthorized.

forbidden

API key is valid but the merchant is suspended.

not_found

The id you passed doesn't exist OR belongs to a different merchant (we don't differentiate to prevent enumeration).

conflict

A uniqueness constraint failed:

  • Slug already taken (on POST /v1/me/slug)
  • Email already registered (on POST /v1/auth/register)
  • Reference already used by this merchant (on POST /v1/payments without idempotency key — returns the existing payment, not an error, but only for exact duplicate)

precondition_failed

Request is well-formed but the resource state doesn't allow the operation:

  • Refunding a payment in failed / processing status
  • Refunding more than the remaining amount
  • Promoting a webhook secret when no rotation is in flight
  • Requesting a payout for more than the available balance

rate_limited

You've exceeded the per-merchant or per-route rate limit. Honor the Retry-After header (seconds) and back off.

internal

Our fault. The message is opaque. Email dev@teronapay.com with the request id (from the X-Request-Id response header) for help.

bad_gateway

A downstream channel rejected synchronously:

  • Daraja credentials wrong / expired
  • Daraja sandbox down
  • Stripe declined with a non-card-decline error (e.g. network)

Retry with backoff. If persistent, check the dashboard for channel-specific status.

Recovery patterns

  • 400 invalid_request: do not retry. Fix the request shape.
  • 401 unauthorized: do not retry. Rotate / re-issue the API key.
  • 422 precondition_failed: do not retry. The resource state is the problem.
  • 429 rate_limited: retry after Retry-After seconds.
  • 500 internal / 502 bad_gateway: retry with exponential backoff. After 3 attempts, surface an error to your end user.

The X-Request-Id header

Every response includes X-Request-Id. Include it when reporting bugs to dev@teronapay.com — we use it to find the request in our logs.

Errors · TeronaPay docs