# Error codes

URL: https://supapulse.app/docs/api/errors

Every error the Supapulse ingest API returns, what causes it, and how to fix it.

## Format [#format]

Most errors are JSON with one `error` field:

```json
{ "error": "Invalid API token" }
```

Validation errors (`422`) use Laravel's format:

```json
{
  "message": "The selected type is invalid.",
  "errors": {
    "type": ["The selected type is invalid."]
  }
}
```

## Status codes [#status-codes]

| Status | Error | Cause | Fix |
| --- | --- | --- | --- |
| `202` | none | Accepted and queued. | Nothing. The body is `{ "success": true }`. |
| `401` | `Missing API token` | No `X-API-Token` header and no `token` field. | Send the token. |
| `401` | `Invalid token format` | The token doesn't start with `pub_` or `sec_`. | Check for quotes, spaces or a truncated value. |
| `401` | `Invalid API token` | No active site has this token. It was regenerated, mistyped, or the site was deleted. | Copy the token again from the site's settings. |
| `402` | `This site's team has no active plan.` | The site's team has no active plan or trial. The body also has `"code": "plan_inactive"`. | Start a plan or trial. |
| `402` | `This team reached its monthly event limit.` | The team used more than its plan's monthly events plus a 10% grace. The body has `"code": "quota_exceeded"`. Not returned for `revenue`. | Upgrade, or wait for the monthly reset. |
| `403` | `Origin not allowed` | The `Origin` (or `Referer`) host isn't the site's domain or a subdomain of it. | Fix the domain in the site's settings, or send from the right domain. |
| `422` | Validation errors | `type` missing or not allowed for the token, `data` missing or not an object, or a `revenue` or `identify` field broke a rule. | See below. |
| `429` | Too Many Attempts | More than 2,000 requests in an hour from one IP address. | Wait for `Retry-After`. |
| `5xx` | | A server problem. | Retry with backoff. |

## 402 responses [#402-responses]

A `402` has two fields, `error` and `code`:

```json
{ "error": "This site's team has no active plan.", "code": "plan_inactive" }
```

| `code` | Meaning |
| --- | --- |
| `plan_inactive` | The team's trial ended or it has no plan. Statuses that can still ingest: active, trial, past due, unpaid and grace period. |
| `quota_exceeded` | The team's monthly event count reached its plan limit plus 10%. Pageviews, events and identify are rejected until the count resets or the plan grows. |

Revenue is never rejected for quota, only for `plan_inactive`. `POST /api/fingerprint` checks the plan only. The Stripe webhook answers `200` with `{ "ignored": "plan_inactive" }` instead of `402`. The SDKs treat `402` as final and don't retry it. Plan checks are cached for up to a minute, so a change takes about that long to apply.

## Validation [#validation]

| Field | Rule |
| --- | --- |
| `type` | Required. `pageview`, `event` or `identify` with a public token; those plus `revenue` with a secret token. |
| `data` | Required. An object. |

Sending `revenue` with a public token fails here with `422`, because `revenue` isn't an allowed type for it.

### Revenue [#revenue]

| Field | Rule |
| --- | --- |
| `data.customer_id` | Required string, up to 255 characters. |
| `data.amount` | Required number from `0` to `99999999.99`. |
| `data.transaction_id` | Required string, up to 255 characters. |
| `data.event_type` | One of `purchase`, `subscription`, `renewal`, `upgrade`, `downgrade`, `churn`, `refund`. |
| `data.currency` | Three letters. |
| `data.visitor_fingerprint`, `data.session_id` | Strings, up to 64 characters. |
| `data.subscription_id` | String, up to 255 characters. |
| `data.mrr_impact` | Number from `-99999999.99` to `99999999.99`. |
| `data.is_recurring` | Boolean. |
| `data.metadata` | Object with up to 50 keys. |
| `data.payment_processor` | String, up to 32 characters. |
| `data.occurred_at` | A date, not more than a day in the future. |

### Identify [#identify]

| Field | Rule |
| --- | --- |
| `data.customer_id` | Required string, up to 255 characters. |
| `data.session_id`, `data.visitor_fingerprint` | Strings, up to 64 characters. |
| `data.source_customer_id` | String, up to 255 characters. |

A `422` response names each failing field in `errors`.

## Errors after the 202 [#errors-after-the-202]

Revenue and identify are validated before they're queued. Pageviews and events aren't, and the worker needs more than the API checks. It drops the event silently if this is missing:

| Type | Needs |
| --- | --- |
| `pageview` | `data.url` |
| `event` | `data.event_name` and `data.url` |

The browser tracker always sends what pageviews and events need. A revenue event with the same `transaction_id` and `event_type` as an earlier one is ignored, not an error.

## Checklist [#checklist]

1. Is the token the right kind? Public for the browser, secret for revenue.
2. Is the site's domain set to the domain the requests come from?
3. Does the request have `type` and a `data` object?
4. For revenue: are `customer_id`, `amount` and `transaction_id` set, and is `transaction_id` new for that event type?
5. A `402`: does the site's team have an active plan, and is it under its monthly limit?
6. Still stuck? See [Debugging](https://supapulse.app/docs/debugging).
