# Errors

When a request fails, Payd answers with an HTTP error status and a JSON body:

```json
{
  "error_code": "insufficient_balance",
  "error_message": "insufficient balance",
  "status_code": 400,
  "request_id": "4bf92f3577b34da6"
}
```

| Field | Meaning |
| --- | --- |
| `error_code` | A stable `snake_case` code. Branch on this. |
| `error_message` | Text you can log or show. It can change, so do not parse it. On a `5xx` it is always a generic message. |
| `status_code` | The HTTP status, repeated. |
| `request_id` | The id of this request. Send it to support when you ask about an error. |

Some errors add extra string fields next to these four. Log them with the rest of the body.

## Codes by HTTP status

| Status | `error_code` | Meaning | What to do |
| --- | --- | --- | --- |
| `400` | `invalid_argument` | A field is missing or not valid. The message names it. | Fix the request. Do not retry it unchanged. |
| `400` | `insufficient_balance` | Your balance does not cover the payout and its fee. | Top up, or pay out less. Check fees with a [quote](./quotes.md). |
| `400` | `provider_error` | The mobile money or bank network refused the payment, for example because the number is not registered. | Check the details with the customer. |
| `400` | `invalid_form` | The network refused a payout after Payd took the money from your balance. Payd has already put it back. | Check the recipient's details, then send a new payout. |
| `400` | `beneficiary_inactive` | The saved recipient you named is switched off. | Use another recipient. |
| `401` | `user_unauthorized` | The API key is missing or wrong. | Check the username and password. See [authentication](./authentication.md). |
| `403` | `permission_denied` | The key cannot act on that account, or the feature is not on for your account, such as card payments. | Use the account id from your key, or contact support. |
| `404` | `resource_not_found` | Payd has no record with that reference for your account, or nothing is set up for that country, currency and channel. | Check the reference or the payment option. |
| `405` | `method_not_allowed` | The path exists but not with this HTTP method. | Check the method. |
| `409` | `duplicate_conflict` | The record already exists, or the `idempotency_key` belongs to another payment, one that failed or one still running. The message says which. | Read the message. Check the status of the reference it names, or use a new key. |
| `429` | `too_many_requests` | You are sending requests too fast. | Wait, then retry with backoff. |
| `499` | `request_cancelled` | Your client closed the connection. | See [retries and duplicates](./core-concepts.md#retries-and-duplicates) before you resend. |
| `500` | `internal_error` | Something failed inside Payd. | Check before you resend a payin or payout. |
| `501` | `not_implemented` | The endpoint does not exist. | Check the path. The [changelog](./changelog.md) lists endpoints that moved. |
| `502` | `bad_gateway` | Payd could not reach an internal service. | Check, then retry. |
| `503` | `service_unavailable` | A service is down or busy. | Check, then retry with backoff. |
| `504` | `gateway_timeout` or `request_timeout` | Payd did not finish in time. The payment may still go through. | [Search your payment requests](./transaction-status.md#search-payment-requests) before you resend. |

"Check" in this table means: for a payin or payout, find out whether the payment exists before you send it again. See [retries and duplicates](./core-concepts.md#retries-and-duplicates).

## Messages you will see on payments

| `error_code` | `error_message` starts with | Cause |
| --- | --- | --- |
| `invalid_argument` | `missing required fields:` | `vendor_config`, `customer`, `provider_config` or `payment_config` is missing, or `vendor_config.account_id` is empty. Send `{}` for an object you have nothing to put in. |
| `invalid_argument` | `payment_config.account_number or customer.phone_number is required` | Payd does not know which phone or account to use. |
| `invalid_argument` | `send customer.customer_uid for a saved beneficiary, or customer.first_name and customer.last_name` | The customer's first or last name is missing. |
| `invalid_argument` | `currency field not populated` | `currency` is missing. |
| `invalid_argument` | `withdrawal pin not set for account` | The key owner has not set a withdrawal PIN in the Payd Money App. |
| `invalid_argument` | `Complete your KYC before sending money to this country` | The account holder's identity details are incomplete in the Payd Money App. |
| `invalid_argument` | `Nigeria needs a second form of identification` | Your account is registered in Nigeria and has no BVN or NIN on file. See [payouts across Africa](./pan-african-payouts.md#nigeria). |
| `permission_denied` | `you do not have access to this account` | `vendor_config.account_id` is not the account your key belongs to. |
| `permission_denied` | `this account is not activated for card payments` | Ask support to switch card payments on. |
| `resource_not_found` | `account not found` | The account id does not exist. |

## The older collection endpoint

`POST /api/v2/payments`, the [older M-Pesa collection endpoint](./kenya-payins.md#the-older-collection-endpoint), keeps the error body it always had:

```json
{
  "status": 400,
  "success": false,
  "message": "number (7458969711) is not valid kenyan number"
}
```

## Handle errors in code

::: code-group

```js [Node.js]
const res = await fetch(url, options)
const body = await res.json()

if (!res.ok) {
  console.error(`Payd ${body.status_code} ${body.error_code} (${body.request_id}): ${body.error_message}`)
  if (body.error_code === 'insufficient_balance') return askForTopUp()
  if (res.status >= 500) return checkBeforeRetrying()
  throw new Error(body.error_message)
}
```

```python [Python]
res = requests.post(url, json=payload, auth=AUTH, timeout=35)
body = res.json()

if not res.ok:
    print(f"Payd {body['status_code']} {body['error_code']} ({body.get('request_id')}): {body['error_message']}")
    if body["error_code"] == "insufficient_balance":
        ask_for_top_up()
    elif res.status_code >= 500:
        check_before_retrying()
    else:
        raise RuntimeError(body["error_message"])
```

:::
