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

# Errors

> Understand error responses, error types, error codes, and how to handle them in your integration

Fingo Pay uses conventional HTTP status codes and returns a consistent JSON error body on every non-2xx response. Each error includes a machine-readable `type` and `code` you can use for programmatic handling, a human-readable `message` for debugging, and a `requestId` you can reference when contacting support.

## Error body

Every error response follows this structure:

```json theme={"dark"}
{
  "error": {
    "type": "invalid_request_error",
    "code": "missing_parameter",
    "message": "Required parameter 'amount' is missing.",
    "param": "amount",
    "requestId": "req_01j7b6f9p5y9h"
  }
}
```

<ResponseField name="type" type="string" required>
  The category of error. Use this to determine the general class of the problem. See [Error types](#error-types) below.
</ResponseField>

<ResponseField name="code" type="string" required>
  A specific, machine-readable error code for programmatic handling. See [Error codes](#error-codes) below.
</ResponseField>

<ResponseField name="message" type="string" required>
  A human-readable description of the error. Intended for developer logs and debugging -- do not display raw error messages to end users.
</ResponseField>

<ResponseField name="param" type="string | null">
  The request parameter that caused the error, when applicable. Useful for highlighting the specific field in your UI or logs. Returns `null` when the error is not tied to a single parameter.
</ResponseField>

<ResponseField name="requestId" type="string">
  A unique identifier for the request. Always include this when contacting Fingo Pay support.
</ResponseField>

## Error types

The `error.type` field tells you the broad category of the problem. Use it to decide how your integration should respond.

<AccordionGroup>
  <Accordion title="invalid_request_error">
    The request was malformed or contained invalid data. This covers missing parameters, bad values, schema validation failures, and missing headers. Check `error.code` and `error.param` for specifics.

    **Typical HTTP status:** `400`, `409`
  </Accordion>

  <Accordion title="authentication_error">
    The request could not be authenticated. The API key is missing, invalid, expired, or the authenticated organization does not have permission to perform the action.

    **Typical HTTP status:** `401`, `403`
  </Accordion>

  <Accordion title="rate_limit_error">
    Your organization has exceeded the allowed request rate. Back off, respect the `Retry-After` header, and retry. See [Rate limits](/rate-limits) for details.

    **Typical HTTP status:** `429`
  </Accordion>

  <Accordion title="not_found_error">
    The requested resource does not exist. This applies to transactions, accounts, shortcodes, and other resources looked up by ID or reference.

    **Typical HTTP status:** `404`
  </Accordion>

  <Accordion title="conflict_error">
    The request conflicts with the current state of a resource. Most commonly returned when an idempotency key is reused with a different request body, path, or method.

    **Typical HTTP status:** `409`
  </Accordion>

  <Accordion title="api_error">
    An unexpected error occurred on the Fingo Pay server. These are rare. If you encounter one, retry with exponential backoff and contact support if it persists.

    **Typical HTTP status:** `500`
  </Accordion>
</AccordionGroup>

## Error codes

The `error.code` field provides a specific, machine-readable identifier for the error. Use it for programmatic branching in your integration.

| `error.code`         | `error.type`            | HTTP | Description                                                                                           |
| -------------------- | ----------------------- | ---- | ----------------------------------------------------------------------------------------------------- |
| `unauthorized`       | `authentication_error`  | 401  | Missing Bearer token or invalid/expired API key.                                                      |
| `forbidden`          | `authentication_error`  | 403  | Authenticated but not allowed (for example, organization not verified for production).                |
| `too_many_requests`  | `rate_limit_error`      | 429  | Per-organization rate limit exceeded. Respect the `Retry-After` header.                               |
| `conflicting_retry`  | `conflict_error`        | 409  | Same idempotency key reused with a different request body, path, or method.                           |
| `validation_error`   | `invalid_request_error` | 400  | Request body, query, or header failed schema validation.                                              |
| `missing_header`     | `invalid_request_error` | 400  | A required header is missing (usually `Idempotency-Key` in the live environment).                     |
| `invalid_parameter`  | `invalid_request_error` | 400  | A parameter is present but has an invalid value (bad format, out of range, or fails a business rule). |
| `missing_parameter`  | `invalid_request_error` | 400  | A required query or body field is missing.                                                            |
| `duplicate`          | `invalid_request_error` | 409  | Duplicate resource or reference (commonly a reused `merchantTransactionId`).                          |
| `resource_not_found` | `not_found_error`       | 404  | The target resource was not found (transaction, account, shortcode, etc.).                            |
| `internal_error`     | `api_error`             | 500  | Server-side failure during processing. Retry with backoff; contact support if it persists.            |

## Sample error responses

Realistic response bodies for the most common error scenarios.

<AccordionGroup>
  <Accordion title="401 — Unauthorized (missing or invalid API key)">
    ```json 401 Unauthorized theme={"dark"}
    {
      "error": {
        "type": "authentication_error",
        "code": "unauthorized",
        "message": "Missing or invalid Authorization header. Provide a valid Bearer token.",
        "requestId": "req_01j7c2a1k9m4x"
      }
    }
    ```
  </Accordion>

  <Accordion title="403 — Forbidden (organization not verified)">
    ```json 403 Forbidden theme={"dark"}
    {
      "error": {
        "type": "authentication_error",
        "code": "forbidden",
        "message": "Your organization is not verified for production access. Complete verification in the dashboard.",
        "requestId": "req_01j7c3b4n7p2y"
      }
    }
    ```
  </Accordion>

  <Accordion title="400 — Missing parameter">
    ```json 400 Bad Request theme={"dark"}
    {
      "error": {
        "type": "invalid_request_error",
        "code": "missing_parameter",
        "message": "Required parameter 'amount' is missing.",
        "param": "amount",
        "requestId": "req_01j7c4d6q3r8z"
      }
    }
    ```
  </Accordion>

  <Accordion title="400 — Invalid parameter">
    ```json 400 Bad Request theme={"dark"}
    {
      "error": {
        "type": "invalid_request_error",
        "code": "invalid_parameter",
        "message": "Amount must be between 1000 and 25000000 (KES 10.00 — KES 250,000.00).",
        "param": "amount",
        "requestId": "req_01j7c5e8s2t7a"
      }
    }
    ```
  </Accordion>

  <Accordion title="400 — Validation error">
    ```json 400 Bad Request theme={"dark"}
    {
      "error": {
        "type": "invalid_request_error",
        "code": "validation_error",
        "message": "phoneNumber must be a valid Kenyan MSISDN in the format +2547XXXXXXXX.",
        "param": "phoneNumber",
        "requestId": "req_01j7c6f9u4v3b"
      }
    }
    ```
  </Accordion>

  <Accordion title="400 — Missing header (Idempotency-Key)">
    ```json 400 Bad Request theme={"dark"}
    {
      "error": {
        "type": "invalid_request_error",
        "code": "missing_header",
        "message": "Idempotency-Key header is required for POST requests in the live environment.",
        "param": "Idempotency-Key",
        "requestId": "req_01j7c7g1w5x2c"
      }
    }
    ```
  </Accordion>

  <Accordion title="409 — Idempotency conflict">
    ```json 409 Conflict theme={"dark"}
    {
      "error": {
        "type": "conflict_error",
        "code": "conflicting_retry",
        "message": "Idempotency-Key '8b47872a-4de9-4c13-b73a-19803f55b1e4' was already used with a different request body.",
        "requestId": "req_01j7c8h3y6z1d"
      }
    }
    ```
  </Accordion>

  <Accordion title="409 — Duplicate merchantTransactionId">
    ```json 409 Conflict theme={"dark"}
    {
      "error": {
        "type": "invalid_request_error",
        "code": "duplicate",
        "message": "A transaction with merchantTransactionId 'invoice_48291' already exists.",
        "param": "merchantTransactionId",
        "requestId": "req_01j7c9j5a7b0e"
      }
    }
    ```
  </Accordion>

  <Accordion title="404 — Resource not found">
    ```json 404 Not Found theme={"dark"}
    {
      "error": {
        "type": "not_found_error",
        "code": "resource_not_found",
        "message": "No transaction found with transactionId 'txn_nonexistent'.",
        "param": "transactionId",
        "requestId": "req_01j7cak7c8d9f"
      }
    }
    ```
  </Accordion>

  <Accordion title="429 — Rate limit exceeded">
    ```json 429 Too Many Requests theme={"dark"}
    {
      "error": {
        "type": "rate_limit_error",
        "code": "too_many_requests",
        "message": "Rate limit exceeded. Retry after 12 seconds.",
        "requestId": "req_01j7cbl9e0f8g"
      }
    }
    ```

    <Info>
      The response includes a `Retry-After` header with the number of seconds to wait before retrying.
    </Info>
  </Accordion>

  <Accordion title="500 — Internal server error">
    ```json 500 Internal Server Error theme={"dark"}
    {
      "error": {
        "type": "api_error",
        "code": "internal_error",
        "message": "An unexpected error occurred while processing your request. Please retry or contact support.",
        "requestId": "req_01j7ccm1g2h7i"
      }
    }
    ```
  </Accordion>
</AccordionGroup>

## Handling errors

Use `error.type` to categorize the problem, `error.code` to decide what your application should do, `error.param` to surface which field failed, and `error.message` for developer-facing logs.

<CodeGroup>
  ```javascript Node.js theme={"dark"}
  async function createCharge(body, idempotencyKey) {
    const res = await fetch('https://api.fingopay.io/v1/mpesa/charge', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.FINGO_API_KEY}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': idempotencyKey,
      },
      body: JSON.stringify(body),
    });

    const data = await res.json();

    if (!res.ok) {
      const { type, code, message, param } = data.error;

      switch (type) {
        case 'authentication_error':
          // Check credentials or org verification status
          throw new Error(`Auth failed: ${message}`);

        case 'invalid_request_error':
          if (code === 'missing_parameter' || code === 'invalid_parameter') {
            console.error(`Fix field '${param}': ${message}`);
          }
          if (code === 'duplicate') {
            console.warn(`Duplicate transaction: ${message}`);
          }
          throw new Error(`Bad request [${code}]: ${message}`);

        case 'rate_limit_error':
          const retryAfter = res.headers.get('Retry-After') || '5';
          await new Promise((r) => setTimeout(r, parseInt(retryAfter, 10) * 1000));
          return createCharge(body, idempotencyKey);

        case 'conflict_error':
          // Generate a new idempotency key if the body changed
          throw new Error(`Conflict: ${message}`);

        case 'not_found_error':
          throw new Error(`Not found: ${message}`);

        case 'api_error':
          // Retry with backoff; contact support if persistent
          throw new Error(`Server error: ${message}`);

        default:
          throw new Error(`Unexpected error: ${message}`);
      }
    }

    return data;
  }
  ```

  ```python Python theme={"dark"}
  import os, requests, time

  def create_charge(body: dict, idempotency_key: str, attempt: int = 0):
      res = requests.post(
          "https://api.fingopay.io/v1/mpesa/charge",
          json=body,
          headers={
              "Authorization": f"Bearer {os.environ['FINGO_API_KEY']}",
              "Content-Type": "application/json",
              "Idempotency-Key": idempotency_key,
          },
      )

      if res.ok:
          return res.json()

      err = res.json()["error"]
      error_type = err["type"]
      code = err["code"]
      message = err["message"]
      param = err.get("param")

      if error_type == "authentication_error":
          raise Exception(f"Auth failed: {message}")

      elif error_type == "invalid_request_error":
          if code in ("missing_parameter", "invalid_parameter"):
              print(f"Fix field '{param}': {message}")
          if code == "duplicate":
              print(f"Duplicate transaction: {message}")
          raise Exception(f"Bad request [{code}]: {message}")

      elif error_type == "rate_limit_error":
          retry_after = int(res.headers.get("Retry-After", "5"))
          time.sleep(retry_after)
          return create_charge(body, idempotency_key, attempt + 1)

      elif error_type == "conflict_error":
          raise Exception(f"Conflict: {message}")

      elif error_type == "not_found_error":
          raise Exception(f"Not found: {message}")

      elif error_type == "api_error":
          if attempt < 3:
              time.sleep(2 ** attempt)
              return create_charge(body, idempotency_key, attempt + 1)
          raise Exception(f"Server error: {message}")

      else:
          raise Exception(f"Unexpected error: {message}")
  ```
</CodeGroup>

<Tip>
  Always log the `requestId` from error responses. Fingo Pay support can use it to trace the exact request in our systems.
</Tip>

## HTTP status codes

A summary of HTTP status codes returned by the Fingo Pay API and what they mean.

| Status | Meaning               | When you see it                                                                                                                         |
| ------ | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `200`  | OK                    | Request succeeded. Response body contains the requested data.                                                                           |
| `202`  | Accepted              | Request accepted for asynchronous processing. Final result arrives via [webhook](/webhooks).                                            |
| `400`  | Bad Request           | The request is invalid -- missing fields, bad values, or failed schema validation.                                                      |
| `401`  | Unauthorized          | No valid API key provided. Check your `Authorization` header.                                                                           |
| `403`  | Forbidden             | Authenticated but not permitted. Your organization may not be verified for this environment.                                            |
| `404`  | Not Found             | The requested resource does not exist. Verify the ID or reference you are looking up.                                                   |
| `409`  | Conflict              | Idempotency conflict or duplicate resource. See `error.code` for details.                                                               |
| `429`  | Too Many Requests     | Rate limit exceeded. Back off and retry after the `Retry-After` interval. See [Rate limits](/rate-limits).                              |
| `500`  | Internal Server Error | Something went wrong on our end. Retry with exponential backoff and [contact support](mailto:integrations@fingo.africa) if it persists. |
