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

# Errors and rate limits

> Response envelopes, status codes, validation details and the request limit.

## Envelopes

Successful responses wrap their payload in `data`:

```json theme={null}
{ "data": { "packageId": "pkg_9f3c2e" } }
```

Errors carry a human-readable `error` and, for validation failures, a `details` map:

```json theme={null}
{
  "error": "Validation failed",
  "details": {
    "recipients.0.email": ["Invalid email"],
    "title": ["Required"]
  }
}
```

Creates return `201`; everything else that succeeds returns `200`.

## Status codes

| Status | When                                                                                                                                                                                                                        |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | The body is not valid JSON (`Invalid JSON body`), or the action is not possible in the package's current state (for example `Only DRAFT packages can be sent`, or a transfer to a user outside the organisation).           |
| `401`  | Missing, malformed, unknown, revoked or expired API key. See [Authentication](/api-reference/authentication).                                                                                                               |
| `402`  | The plan does not include the feature (public API, webhooks), the organisation has no credit left to send, or a plan limit such as recipients per package or number of templates would be exceeded. The message says which. |
| `403`  | The caller's IP address is outside the organisation's allowlist, the key's user lacks a permission (`Missing permission: canSendPackages`), or the endpoint needs an organisation admin.                                    |
| `404`  | The package, template or webhook endpoint does not exist in the workspace or organisation the call acted in.                                                                                                                |
| `422`  | The request was well-formed JSON but failed schema validation. `details` is keyed by field path.                                                                                                                            |
| `429`  | Rate limit exceeded. See below.                                                                                                                                                                                             |
| `500`  | Something went wrong on our side. Retry with backoff; if it persists, contact [support@flowsign.app](mailto:support@flowsign.app).                                                                                          |

## Validation details

`details` keys are dotted paths into the request. Array items are addressed by index, so `recipients.0.email` is the `email` of the first recipient. Query parameters are validated the same way: `GET /api/v1/packages?pageSize=500` returns `422` with `details.pageSize`.

A `422` on a body that references something the target resource does not have (an unknown `roleIndex`, for example) uses the same shape.

## Rate limits

All `/api/v1` routes share one limit: **120 requests per 60-second window**, counted per source IP address. Beyond it the API returns:

```http theme={null}
HTTP/1.1 429 Too Many Requests
Retry-After: 37
Content-Type: application/json

{ "error": "Rate limit exceeded" }
```

`Retry-After` is the number of seconds until the window resets. Wait at least that long before retrying.

<CodeGroup>
  ```javascript JavaScript theme={null}
  async function callWithRetry(url, init, attempts = 3) {
    for (let i = 0; i < attempts; i++) {
      const res = await fetch(url, init);
      if (res.status !== 429) return res;
      const wait = Number(res.headers.get("Retry-After") ?? "1");
      await new Promise((r) => setTimeout(r, wait * 1000));
    }
    throw new Error("Rate limited after retries");
  }
  ```

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

  import requests


  def call_with_retry(session, method, url, attempts=3, **kwargs):
      for _ in range(attempts):
          res = session.request(method, url, timeout=30, **kwargs)
          if res.status_code != 429:
              return res
          time.sleep(float(res.headers.get("Retry-After", "1")))
      raise RuntimeError("Rate limited after retries")
  ```

  ```csharp C# theme={null}
  using System.Net;

  static async Task<HttpResponseMessage> CallWithRetryAsync(
      HttpClient http,
      Func<HttpRequestMessage> newRequest,
      int attempts = 3)
  {
      for (var i = 0; i < attempts; i++)
      {
          var res = await http.SendAsync(newRequest());
          if (res.StatusCode != HttpStatusCode.TooManyRequests) return res;

          var wait = res.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(1);
          await Task.Delay(wait);
      }

      throw new HttpRequestException("Rate limited after retries");
  }
  ```
</CodeGroup>

<Tip>
  Prefer [webhooks](/webhooks/overview) over polling `GET /api/v1/packages/{packageId}` to stay well under the limit.
</Tip>
