# Errors

> Every error the Railbed API returns, with its HTTP status, what caused it and what to do next.

Source: https://railbed.io/docs/api/errors/ · Updated: 2026-09-26 · Railbed by DeepWork developer docs

## The error object

Every error has an HTTP status of 400 or above and the same JSON body:

An error · response 409 Conflict:

```json
{
  "error": {
    "code": "no_payout_wallet",
    "message": "Add a payout wallet in the dashboard before taking live payments."
  }
}
```

| Field | Type | Description |
|---|---|---|
| `code` | string | Stable and machine-readable. Branch on this |
| `message` | string | A sentence for people. It can be reworded at any time, so show it or log it, but never parse it |
| `field` | string | The request field at fault, when there is one: `amount`, `metadata`, `starting_after` |

## Handling errors

Decide by status first, then by `code` where it matters:

| Status | Meaning | What your code should do |
|---|---|---|
| `400` | The request was invalid | Fix the request. Retrying it unchanged fails the same way |
| `401` | The key is missing, malformed or revoked | Check the key and its mode. Don't retry |
| `403` | Not allowed for this account or key | Don't retry |
| `404` | Not found in this account and mode | Check the id and whether the key's mode matches the object's |
| `409` | The object's state doesn't allow this | Read the object and act on its current status |
| `410` | The session can't be paid any more | Create a new session |
| `413`, `415` | The body is too large or isn't JSON | Fix the request |
| `429` | Rate limited | Wait for `Retry-After` seconds, then retry |
| `500`, `502`, `503` | Something failed on our side or upstream | Retry with backoff. Creates are safe to retry with the same `Idempotency-Key` |

A small error handler:

```js
async function railbed(path, init = {}) {
  const res = await fetch(`https://pay.railbed.io/v1${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${process.env.RAILBED_SECRET_KEY}`,
      'Content-Type': 'application/json',
      ...init.headers,
    },
  });
  const body = await res.json();
  if (res.ok) return body;
  const err = Object.assign(new Error(body.error.message), {
    status: res.status,
    code: body.error.code,
    field: body.error.field,
  });
  err.retryable = res.status === 429 || res.status >= 500;
  err.retryAfter = Number(res.headers.get('Retry-After')) || null;
  throw err;
}
```

## Every code

### 400 Bad Request

| Code | Endpoint | Cause |
|---|---|---|
| `invalid_json` | Any with a body | The body isn't a JSON object |
| `invalid_amount` | Create | `amount` isn't a string, or isn't from `"1.00"` to `"100000.00"` with at most two decimal places |
| `invalid_currency` | Create | `currency` isn't `USD`, `EUR`, `GBP`, `CAD` or `AUD` |
| `missing_description` | Create | `description` is missing, blank or not a string |
| `too_long` | Create | `description` or `reference` is over 120 characters. See `field` |
| `invalid_reference` | Create | `reference` isn't a string |
| `invalid_email` | Create, Start | `customer_email` isn't a valid address, or Start needs one and none is on the session |
| `invalid_url` | Create | `success_url` or `cancel_url` isn't a full `http(s)://` address, has a username or password, is over 1,000 characters, or isn't `https://` in Live mode |
| `invalid_metadata` | Create | Over 20 keys, a blank key, a key over 40 characters or starting with `__`, or a value that isn't a string of at most 500 characters |
| `invalid_idempotency_key` | Create | The `Idempotency-Key` header is blank, over 120 characters or has characters other than printable ASCII |
| `invalid_country` | Start | `country` isn't a two-letter code such as `US` |
| `invalid_limit` | List payments | `limit` isn't a whole number from 1 to 100 |
| `invalid_cursor` | List payments | `starting_after` isn't one of your payments in this mode |
| `invalid_outcome` | Simulate | `outcome` isn't `paid`, `underpaid` or `failed` |

### 401 Unauthorized

| Code | Cause |
|---|---|
| `invalid_api_key` | No `Authorization: Bearer` header, a malformed key, or a revoked one. Create a key in [Developers](https://app.railbed.io/developers) |

### 403 Forbidden

| Code | Cause |
|---|---|
| `suspended` | The account is suspended. Contact support |
| `live_payment` | Simulate was called on a Live payment. Only Test payments can be simulated |

### 404 Not Found

| Code | Cause |
|---|---|
| `not_found` | No such session or payment in your account in this key's mode, or no such endpoint. A Test key can't see Live objects and the other way round |

### 409 Conflict

| Code | Endpoint | Cause |
|---|---|---|
| `idempotency_conflict` | Create | The key was used with a different body. Rarely, the first request with the key hadn't finished saving: retry in a moment |
| `no_payout_wallet` | Create | Live mode needs a payout wallet. Add one in [Settings](https://app.railbed.io/settings) |
| `unavailable` | Start | The account can't take payments now |
| `no_providers` | Start | No card provider can take this amount and currency right now. Nothing was started; try later or a different amount |
| `already_paid` | Start | The payment is complete |
| `held` | Start | The money arrived and the payment is held for review |
| `failed` | Start | The payment was declined. Create a new session |
| `not_started` | Simulate | Start the session before simulating |

### 410 Gone

| Code | Cause |
|---|---|
| `expired` | The session passed its `expires_at` without being paid |
| `canceled` | The payment link was canceled in the dashboard |

### Size, type and rate

| Status | Code | Cause |
|---|---|---|
| 413 | `request_too_large` | The body is over 64 KiB |
| 415 | `unsupported_media_type` | The body isn't sent as `Content-Type: application/json` |
| 429 | `rate_limited` | Over about 120 requests a minute, or 12 starts a minute. Wait for `Retry-After` |

### 5xx

| Status | Code | Cause |
|---|---|---|
| 500 | `internal` | Something failed on our side. Retry with backoff; contact support if it continues |
| 502 | `network_unavailable` | The card network didn't answer while starting. Retry in a minute |
| 503 | `misconfigured` | Card payments are briefly unavailable. Retry later |

> [!NOTE]
> New codes can be added within `v1`. Treat an unknown `code` by its status: an unknown `4xx` is a request to fix, an unknown `5xx` a reason to retry.
