Now inviting stores. Join the waitlist to hear the agent call you.

Join the waitlist

Search the docs

Find a docs page

API reference

Handling errors

Preview

How the RowCall API reports errors, which to fix, which to retry and how, and how to branch on the stable error code.

Every response has ok. When it is false, you also get a sentence you can log or show (error) and a stable code you can branch on (errorCode), with an HTTP status that matches.

{ "ok": false, "error": "Fill in Order.", "errorCode": "INVALID_INPUT" }

Every code, its status, what it means and what to do is in the error table. This page is about handling them well.

Branch on the code, not the words

error is written for people and may be reworded. errorCode never changes meaning, so branch on it. New codes may be added: treat a code you don't know like a generic failure of its status.

import { RowCallError } from '@rowcall/sdk'

try {
  await rowcall.runs.start(input, { idempotencyKey: `verify-${order.id}` })
} catch (error) {
  if (!(error instanceof RowCallError)) throw error
  if (error.errorCode === 'NO_CONSENT' || error.errorCode === 'OPTED_OUT') return markOrder(order, 'call-not-allowed')
  if (error.status === 429 || error.status >= 500) return retryLater(order)
  throw error
}

Fix, wait, or retry

Errors fall into three kinds:

  • Fix the request. INVALID_INPUT, FORBIDDEN, NOT_FOUND, IDEMPOTENCY_MISMATCH and the like: the same request will fail again. error names what's wrong, such as the missing input. Log it and fix the cause; don't retry as is.
  • The shopper or your account isn't ready. A run refused as NO_CONSENT, OPTED_OUT, NOT_PUBLISHED, NOT_VERIFIED, NO_NUMBER or NO_CREDIT made no run and placed no call. Show the reason in your own system: record consent at checkout, respect the opt-out, publish the flow, or top up. Test runs are never refused for verification, numbers or credit.
  • Try again later. RATE_LIMITED (429), IDEMPOTENCY_IN_PROGRESS (409) and any 5xx are worth retrying, with backoff and a little random jitter, and the same Idempotency-Key.

Retrying safely

A POST that timed out may have worked. Retry it with the same Idempotency-Key, and you get the first response back instead of a second call. A request that was refused is kept too, so after you fix the cause, use a new key. A 5xx isn't kept, so retrying it runs the request again. See Idempotency.

Back off between tries (for example 1, 2, 4, 8 seconds), give up after a few, and alert someone when you do.

When a run stops later

Not every problem shows up as a refused request. A run that was accepted can wait, keeping its attempts, with the reason in lastError: for credit, for business verification, or for one of your org's numbers to be declared for automated calls on DLT (NUMBER_NOT_DECLARED; RowCall declares them). See Registration (DLT).

A run that later stops without reaching the shopper ends as gave_up or canceled, with a sentence in lastError and a run.failed or run.canceled event. For example: every attempt went unanswered, the shopper opted out, consent no longer allows the call, or calling hours never opened. Handle these from your webhook, not from the request.

OAuth errors

The OAuth endpoints (/v1/oauth/token, /v1/oauth/revoke) follow the OAuth standard instead: { "error": "invalid_grant", "error_description": "…" }. See Build an integration.

Next stepRate limits

How many requests you can send.