> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pazy.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors and retries

> How Pazy reports errors, what common codes mean, and which requests are safe to retry

## Error format

Most error responses have `ok: false` and an `error` object with a machine-readable `code` and a human-readable `message`.

```json theme={null}
{
  "ok": false,
  "error": {
    "code": "VENDOR_NOT_FOUND",
    "message": "Vendor not found"
  }
}
```

Branch on `code`, not `message`. Messages can change.

<Note>
  The duplicate idempotency key error is the exception. It returns `409` with `error` as a plain string, for example `{ "error": "Duplicate request: idempotency key already used" }`. Read the HTTP status first, then parse the body defensively.
</Note>

## HTTP status codes

| Status | Meaning | Common codes |
| - | - | - |
| `400` | The request is invalid | `VALIDATION_ERROR`, `MISSING_REQUIRED_FIELD`, plus field-specific codes such as `INVALID_GST_NUMBER` |
| `401` | Authentication failed | `MISSING_CREDENTIALS`, `INVALID_API_KEY` |
| `403` | Authenticated but not allowed | `INSUFFICIENT_PERMISSIONS`, `ACCESS_DENIED` |
| `404` | The resource doesn't exist | `VENDOR_NOT_FOUND`, `INVOICE_NOT_FOUND`, `PURCHASE_ORDER_NOT_FOUND` |
| `409` | Conflict with current state | `MAP_BATCH_IN_PROGRESS`, or a repeated idempotency key |
| `413` | Uploaded file too large | |
| `422` | Valid request, but nothing succeeded | Returned by mark-as-paid when every invoice fails |
| `429` | Rate limit exceeded | `RATE_LIMIT_EXCEEDED` |
| `500` | Something went wrong on our side | `INTERNAL_ERROR` |

Each endpoint page lists the exact errors it can return.

## Fixing common errors

<AccordionGroup>
  <Accordion title="401: MISSING_CREDENTIALS or INVALID_API_KEY">
    Send the key as `Authorization: Api-Key YOUR_API_KEY`. Check for a missing `Api-Key ` prefix, extra whitespace, or a key that was deleted. You can create a new key in the [Pazy app](https://app.pazy.io) under **Settings → Developer**.
  </Accordion>

  <Accordion title="403: INSUFFICIENT_PERMISSIONS">
    The key is valid but lacks permission for this resource and action. Endpoint pages state what they need, for example **Payment** with **Create**. Create a key with the right permissions.
  </Accordion>

  <Accordion title="400: VALIDATION_ERROR">
    The `message` names the failing field. Compare your payload with the endpoint's parameter table, paying attention to formats such as `YYYY-MM-DD` dates, uppercase state codes like `MAHARASHTRA`, and 15-character GSTINs.
  </Accordion>

  <Accordion title="404 on a resource you just created">
    Use the identifier returned by the create call: `vendorId` for vendors, `poId` for purchase orders, and `id` for invoices.
  </Accordion>
</AccordionGroup>

## What to retry

| Response | Retry? |
| - | - |
| `400`, `401`, `403`, `404`, `413` | No. Fix the request first. Retrying unchanged will fail the same way. |
| `409` | Usually no. `MAP_BATCH_IN_PROGRESS` means wait for the running batch. A duplicate idempotency key means the first request was already received, so check its outcome instead of resending. |
| `429` | Yes, after waiting for the time in the `Retry-After` header. |
| `500` | Yes, a limited number of times. |
| Network timeout, no response | Yes, but only if the request is idempotent. See below. |

For `429`, wait for the number of seconds in the `Retry-After` header. For `500` and network errors, use exponential backoff with some random jitter, for example waiting 1, 2, 4, then 8 seconds, and stop after a handful of attempts.

```javascript theme={null}
async function withRetry(doRequest, maxAttempts = 5) {
  for (let attempt = 1; ; attempt++) {
    const response = await doRequest();
    const retryable = response.status === 429 || response.status >= 500;
    if (!retryable || attempt === maxAttempts) return response;
    const retryAfter = Number(response.headers.get('Retry-After'));
    const delayMs = retryAfter > 0
      ? retryAfter * 1000
      : 2 ** (attempt - 1) * 1000 + Math.random() * 250;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }
}
```

<Note>
  This page covers requests you send to Pazy. For how Pazy retries webhook deliveries to your server, see [Delivery and retries](/webhooks/delivery-and-retries).
</Note>

## Retrying writes safely

`GET` requests are always safe to retry. Writes need care, because a timeout doesn't tell you whether the request was applied.

* **Mark Invoice as Paid** requires an `x-idempotency-key`. Sending the same key again within 5 minutes returns `409 Conflict` and is not processed, which protects you from double payments. If a request times out, don't resend it blindly. Check the invoice with [Invoice Details](/apis/invoice-details) to see whether the payment was recorded. Only send a new payment, with a new key, if it wasn't.
* **Vendor Advance Creation** accepts an optional `x-idempotency-key`, and we recommend it. A repeat of the same key within 5 minutes returns `409 Conflict` instead of creating a second advance.
* **Other create endpoints** don't document idempotency. Before retrying after a timeout, look up what you were creating, for example with [Vendor Search](/apis/vendor-search), to avoid duplicates. Vendor creation also rejects duplicate names and GSTINs.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.