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

# Errors & Rate Limits

> Understand API error responses, HTTP status codes, rate limiting, and best practices for resilient integrations.

## Error Response Format

When a request fails, the API returns a JSON object with an `error` key. The value is either a single string or an array of strings describing what went wrong.

**Single error:**

```json theme={null}
{
  "error": "Template not found"
}
```

**Multiple errors (typically validation failures):**

```json theme={null}
{
  "error": [
    "Email can't be blank",
    "Role must match a template submitter",
    "Name is too long (maximum is 255 characters)"
  ]
}
```

## HTTP Status Codes

| Status Code | Meaning               | Description                                                                                                                                                                                                                       |
| ----------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`       | Bad Request           | The request body is malformed, a required parameter is missing, or a parameter has an invalid type. Check the error message for specifics.                                                                                        |
| `401`       | Unauthorized          | The `X-Auth-Token` header is missing, empty, or contains an invalid API token.                                                                                                                                                    |
| `403`       | Forbidden             | The API token is valid but lacks permission for this action. This can occur when a token's scopes do not include the requested resource, when your plan does not support the feature, or when IP allowlisting blocks the request. |
| `404`       | Not Found             | The requested resource does not exist or is not accessible to your account.                                                                                                                                                       |
| `422`       | Unprocessable Entity  | The request was well-formed but contained semantic errors. Typically returned for validation failures such as invalid field values, duplicate entries, or constraint violations.                                                  |
| `429`       | Too Many Requests     | You have exceeded the rate limit. Wait and retry after the interval specified in the `Retry-After` header.                                                                                                                        |
| `500`       | Internal Server Error | An unexpected error occurred on the DocuTrust server. If this persists, contact support with the `X-Request-Id` header value from the response.                                                                                   |

## Error Examples by Status Code

### 400 Bad Request

```json theme={null}
{
  "error": "Invalid JSON in request body"
}
```

### 401 Unauthorized

```json theme={null}
{
  "error": "Unauthorized: invalid or missing API token"
}
```

### 403 Forbidden

Standard permission error:

```json theme={null}
{
  "error": "Forbidden: token scope 'read_submissions' does not permit this action"
}
```

Plan limit reached (includes an upgrade URL):

```json theme={null}
{
  "error": "Plan limit reached: your current plan allows 50 submissions per month",
  "upgrade_url": "https://your-instance.spitshake.io/settings/billing/upgrade"
}
```

### 404 Not Found

```json theme={null}
{
  "error": "Template not found"
}
```

### 422 Unprocessable Entity

```json theme={null}
{
  "error": [
    "Email can't be blank",
    "Role must match a template submitter"
  ]
}
```

### 429 Too Many Requests

```json theme={null}
{
  "error": "Rate limit exceeded. Retry after 32 seconds."
}
```

The response also includes the following headers:

| Header                  | Description                                       | Example      |
| ----------------------- | ------------------------------------------------- | ------------ |
| `Retry-After`           | Seconds to wait before making another request.    | `32`         |
| `X-RateLimit-Limit`     | Maximum requests allowed per window.              | `120`        |
| `X-RateLimit-Remaining` | Requests remaining in the current window.         | `0`          |
| `X-RateLimit-Reset`     | Unix timestamp when the rate limit window resets. | `1712674352` |

### 500 Internal Server Error

```json theme={null}
{
  "error": "Internal server error. Reference ID: req_8f3a2b1c-d4e5-6789-abcd-ef0123456789"
}
```

## Rate Limits

The DocuTrust API enforces rate limits to ensure fair usage and platform stability.

| Limit               | Value                                 |
| ------------------- | ------------------------------------- |
| Requests per minute | **120** per API token                 |
| Rate limit window   | 60 seconds (rolling)                  |
| Scope               | Per API token (not per IP or account) |

When you exceed the limit, the API returns a `429 Too Many Requests` response. The `Retry-After` header tells you how many seconds to wait before retrying.

### Rate Limit Headers

Every API response includes rate limit headers so you can track your usage proactively:

| Header                  | Description                                                    |
| ----------------------- | -------------------------------------------------------------- |
| `X-RateLimit-Limit`     | The maximum number of requests permitted per 60-second window. |
| `X-RateLimit-Remaining` | The number of requests remaining in the current window.        |
| `X-RateLimit-Reset`     | The Unix epoch timestamp when the current window resets.       |

## Plan Limits

Some API actions are restricted by your account's subscription plan. When you exceed a plan limit, the API returns a `403 Forbidden` response that includes an `upgrade_url` field pointing to your billing page:

```json theme={null}
{
  "error": "Plan limit reached: your current plan allows 50 submissions per month",
  "upgrade_url": "https://your-instance.spitshake.io/settings/billing/upgrade"
}
```

Common plan-limited resources include:

* Monthly submission count
* Number of templates
* Number of API tokens
* Number of team members
* File storage capacity

## Best Practices

### Exponential Backoff

When you receive a `429` or `5xx` response, implement exponential backoff with jitter to avoid thundering herd problems:

<CodeGroup>
  ```javascript Node.js theme={null}
  async function requestWithBackoff(url, options, maxRetries = 5) {
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      const response = await fetch(url, options);

      if (response.ok) {
        return response.json();
      }

      if (response.status === 429 || response.status >= 500) {
        if (attempt === maxRetries) {
          throw new Error(`Request failed after ${maxRetries + 1} attempts: ${response.status}`);
        }

        const retryAfter = response.headers.get('Retry-After');
        const baseDelay = retryAfter
          ? parseInt(retryAfter, 10) * 1000
          : Math.pow(2, attempt) * 1000;
        const jitter = Math.random() * 1000;
        const delay = baseDelay + jitter;

        await new Promise((resolve) => setTimeout(resolve, delay));
        continue;
      }

      const body = await response.json();
      throw new Error(`API error ${response.status}: ${JSON.stringify(body)}`);
    }
  }
  ```

  ```python Python theme={null}
  import time
  import random
  import requests

  def request_with_backoff(url: str, headers: dict, max_retries: int = 5) -> dict:
      for attempt in range(max_retries + 1):
          response = requests.get(url, headers=headers)

          if response.ok:
              return response.json()

          if response.status_code in (429, 500, 502, 503, 504):
              if attempt == max_retries:
                  response.raise_for_status()

              retry_after = response.headers.get("Retry-After")
              if retry_after:
                  base_delay = int(retry_after)
              else:
                  base_delay = (2 ** attempt)

              jitter = random.uniform(0, 1)
              time.sleep(base_delay + jitter)
              continue

          response.raise_for_status()
  ```

  ```ruby Ruby theme={null}
  require 'net/http'
  require 'json'

  def request_with_backoff(uri, headers, max_retries: 5)
    (0..max_retries).each do |attempt|
      request = Net::HTTP::Get.new(uri)
      headers.each { |k, v| request[k] = v }

      response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
        http.request(request)
      end

      return JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)

      if %w[429 500 502 503 504].include?(response.code)
        raise "Request failed after #{max_retries + 1} attempts" if attempt == max_retries

        retry_after = response['Retry-After']
        base_delay = retry_after ? retry_after.to_i : (2**attempt)
        jitter = rand
        sleep(base_delay + jitter)
        next
      end

      raise "API error #{response.code}: #{response.body}"
    end
  end
  ```
</CodeGroup>

## Idempotency

Create and send operations accept an `Idempotency-Key` header to prevent duplicate requests on retry.

| Status            | Meaning                                                                                                              |
| ----------------- | -------------------------------------------------------------------------------------------------------------------- |
| Original response | A repeated request with the same key and body replays the stored response (with `Idempotency-Replayed: true` header) |
| `422`             | The same key was used with a different request body                                                                  |
| `409`             | A request with this key is still being processed                                                                     |

Keys expire after 24 hours. Keys are bound to the calling token — a different token cannot replay another's cached response.

```bash theme={null}
curl -X POST https://your-instance.com/api/submissions \
  -H "X-Auth-Token: YOUR_TOKEN" \
  -H "Idempotency-Key: unique-request-id-123" \
  -H "Content-Type: application/json" \
  -d '{"template_id": 1, "submitters": [...]}'
```

### Error Handling Checklist

<AccordionGroup>
  <Accordion title="Always check the HTTP status code">
    Do not assume every response is successful. Check the status code before parsing the response body as a success payload.
  </Accordion>

  <Accordion title="Parse both error formats">
    The `error` field may be a string or an array of strings. Handle both cases in your error handling logic.
  </Accordion>

  <Accordion title="Log the X-Request-Id header">
    Every API response includes an `X-Request-Id` header. Log this value so you can reference it when contacting support about `500` errors.
  </Accordion>

  <Accordion title="Respect Retry-After headers">
    When rate-limited, always wait at least as long as the `Retry-After` header specifies. Ignoring this header will extend your rate limit window.
  </Accordion>

  <Accordion title="Do not retry 400, 401, 403, or 404 errors">
    These errors indicate a problem with the request itself. Retrying the same request will produce the same error. Fix the request parameters before retrying.
  </Accordion>

  <Accordion title="Surface upgrade_url to end users">
    When you receive a `403` with an `upgrade_url`, display a clear message to the user explaining the plan limit and providing a link to upgrade.
  </Accordion>
</AccordionGroup>
