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

# Error handling

> Understand error responses and handle them gracefully.

## Error format

All errors follow a consistent JSON structure:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "error": {
    "code": "validation_error",
    "message": "The field 'title' is required.",
    "details": [
      { "field": "title", "message": "Required" }
    ]
  }
}
```

| Field     | Type   | Description                                    |
| --------- | ------ | ---------------------------------------------- |
| `code`    | string | Machine-readable error code (see table below). |
| `message` | string | Human-readable error description.              |
| `details` | array  | Optional. Field-level validation errors.       |

## HTTP status codes

| Status | Code                  | When it happens                                               |
| ------ | --------------------- | ------------------------------------------------------------- |
| `200`  | —                     | Success (GET, PATCH)                                          |
| `201`  | —                     | Resource created (POST)                                       |
| `204`  | —                     | Success with no body (DELETE)                                 |
| `400`  | `validation_error`    | Invalid request body or parameters                            |
| `401`  | `unauthorized`        | Missing or invalid API key                                    |
| `403`  | `forbidden`           | API key doesn't have the required scope                       |
| `404`  | `not_found`           | Resource doesn't exist or doesn't belong to your organization |
| `429`  | `rate_limit_exceeded` | Too many requests — slow down                                 |
| `500`  | `internal_error`      | Something went wrong on our end                               |

## Error codes reference

<AccordionGroup>
  <Accordion title="unauthorized (401)">
    Your API key is missing, invalid, or expired.

    **Common causes:**

    * Missing `Authorization` header
    * Typo in the API key
    * Key was revoked
    * Key has expired

    **Fix:** Check your API key and make sure it's included as `Bearer pp_live_...` in the Authorization header.
  </Accordion>

  <Accordion title="forbidden (403)">
    Your API key is valid but doesn't have permission for this action.

    **Common causes:**

    * Calling a write endpoint with a read-only key
    * Missing the required scope for this resource

    **Fix:** Create a new API key with the required scopes, or update the existing key's permissions.
  </Accordion>

  <Accordion title="not_found (404)">
    The requested resource doesn't exist.

    **Common causes:**

    * Invalid UUID in the URL
    * Resource was deleted
    * Resource belongs to a different organization

    **Fix:** Verify the resource ID. Remember that API keys are scoped to one organization — you can't access resources from other organizations.
  </Accordion>

  <Accordion title="validation_error (400)">
    The request body or query parameters are invalid.

    **Example response:**

    ```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
    {
      "error": {
        "code": "validation_error",
        "message": "Request validation failed.",
        "details": [
          { "field": "title", "message": "Required" },
          { "field": "slug", "message": "String must contain at least 1 character(s)" }
        ]
      }
    }
    ```

    **Fix:** Check the `details` array for specific field-level errors and correct your request.
  </Accordion>

  <Accordion title="rate_limit_exceeded (429)">
    You've exceeded your rate limit for this time window.

    **Fix:** Wait until the `Retry-After` header value (in seconds) has elapsed, then retry. See [Rate Limiting](/api-reference/rate-limiting).
  </Accordion>

  <Accordion title="internal_error (500)">
    An unexpected error occurred on our servers.

    **Fix:** Retry the request after a brief delay. If the error persists, contact support at [contact@propal.io](mailto:contact@propal.io) with the request details.
  </Accordion>
</AccordionGroup>

## Handling errors in code

<CodeGroup>
  ```javascript Node.js theme={"theme":{"light":"github-light","dark":"github-dark"}}
  async function createLead(apiKey, leadData) {
    const response = await fetch('https://api.propal.io/v1/leads', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(leadData),
    });

    if (!response.ok) {
      const { error } = await response.json();

      switch (response.status) {
        case 401:
          throw new Error('Invalid API key — check your credentials');
        case 403:
          throw new Error(`Missing permission: ${error.message}`);
        case 429:
          const retryAfter = response.headers.get('Retry-After');
          console.log(`Rate limited. Retry after ${retryAfter}s`);
          break;
        default:
          throw new Error(`API error: ${error.message}`);
      }
    }

    return response.json();
  }
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import requests
  import time

  def create_lead(api_key, lead_data):
      response = requests.post(
          'https://api.propal.io/v1/leads',
          headers={
              'Authorization': f'Bearer {api_key}',
              'Content-Type': 'application/json',
          },
          json=lead_data,
      )

      if response.status_code == 429:
          retry_after = int(response.headers.get('Retry-After', 60))
          print(f"Rate limited. Waiting {retry_after}s...")
          time.sleep(retry_after)
          return create_lead(api_key, lead_data)  # Retry

      response.raise_for_status()
      return response.json()
  ```
</CodeGroup>

## Retry strategy

For transient errors (`429`, `500`), we recommend exponential backoff:

1. Wait **1 second**, retry
2. Wait **2 seconds**, retry
3. Wait **4 seconds**, retry
4. Wait **8 seconds**, retry
5. Give up after 4 retries

For `429` responses, always respect the `Retry-After` header instead of using your own delay.
