# Errors and rate limits

Every status code the API returns, what causes it, and the rate limit you are working within.

Errors return a matching HTTP status and a JSON body with a single `error` string. There is
no error code enum; the status plus the message is the contract.

```
{ "error": "Package already used" }
```

## Status codes

| Status | Meaning | What to do |
| ------ | --------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `200` | Success | — |
| `201` | Campaign created | Returned only by `POST /campaigns` |
| `400` | Bad request | A required field is missing, the chain is unknown, or the package group does not match the chain. Nothing was consumed. |
| `401` | Authentication failed | Key missing, malformed, or revoked. Check the header. |
| `403` | Account suspended | Contact support. |
| `404` | Not found | The package does not belong to your account, or the token address does not resolve on that chain. |
| `405` | Method not allowed | The `Allow` header lists what the endpoint accepts. |
| `409` | Conflict | The package was already used, most often by a duplicate request. Nothing was double-spent. |
| `429` | Rate limit reached | Back off and retry. |
| `500` | Server error | Retry once. If it persists, send the time and endpoint to support. |

## Rate limit

120 requests per minute per account, across all endpoints and all of your keys.
Exceeding it returns:

```
{ "error": "Rate limit reached, 120 requests per minute" }
```

The limit is generous for what the API does — campaigns change state over hours, not seconds. If you
are hitting it, you are almost certainly polling harder than you need to.

## Duplicate launches

A package can only be consumed once. If a launch request is retried after a timeout and the first one
actually succeeded, the second returns `409` rather than creating a second campaign.

If a launch times out, do not blindly retry. Call `GET /campaigns` first and check whether
the campaign exists.

## Handling errors

```
async function dt(path, options = {}) {
  const res = await fetch(BASE + path, {
    ...options,
    headers: { "X-API-Key": KEY, "Content-Type": "application/json", ...(options.headers || {}) }
  });

  const body = await res.json().catch(() => ({}));

  if (res.status === 429) {
    await new Promise(r => setTimeout(r, 60000));
    return dt(path, options);
  }
  if (!res.ok) {
    const err = new Error(body.error || `HTTP ${res.status}`);
    err.status = res.status;
    throw err;
  }
  return body;
}
```

Retry `429` and `500`. Do not retry `400`, `401`,
`404` or `409` — those describe a request that will fail identically the second
time.

## Questions

**What is the rate limit?**

120 requests per minute per account, shared across all endpoints and all of your keys.

**My launch request timed out. Should I retry?**

Check GET /campaigns first. If the campaign exists the launch succeeded, and a retry would return 409 rather than creating a second one.

**Which errors are worth retrying?**

Only 429 and 500. A 400, 401, 404 or 409 describes a request that will fail the same way again.

