# REST API

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

The ingest endpoint the SDKs use. Call it directly from any language or platform.

```text
POST https://supapulse.app/api/ingest
```

JSON in, JSON out, rate limited to 2,000 requests per hour per IP address. To read your analytics back out, use the [stats API](https://supapulse.app/docs/api/stats) or the [MCP server](https://supapulse.app/docs/api/mcp).

## Authentication [#authentication]

Send your token in the `X-API-Token` header, or as a `token` field in the body. The header wins if both are set. Which token you need depends on the event type.

| Token | Prefix | Allowed types | Where it lives |
| --- | --- | --- | --- |
| Public | `pub_` | `pageview`, `event`, `identify`, `engagement` | Browser. Requests with an `Origin` or `Referer` must come from the site's domain or a subdomain. |
| Secret | `sec_` | `pageview`, `event`, `identify`, `engagement`, `revenue` | Server only. Never ship it to the client. |

## Request body [#request-body]

Every request has a `type` and a `data` object. The fields of `data` depend on the type. Fields not listed are ignored.

```json
{
  "type": "pageview",
  "data": { "url": "https://acme.com/pricing", "session_id": "6f1c…" }
}
```

The body can be sent as `application/json`, or as `text/plain` containing JSON. The browser tracker uses `text/plain` with `sendBeacon` to avoid a CORS preflight.

### pageview [#pageview]

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | Yes | Full URL of the page. Path and UTM parameters are read from it. |
| `referrer` | string | No | `document.referrer`. Its domain becomes the source. |
| `session_id` | string | No | From `getSessionId()`. Up to 64 characters. |
| `locale` | string | No | Browser language, like `en-US`. |

The browser tracker also sends `path`, `title`, `referrer_source` and `utm_*`; the server derives these itself and doesn't use the values sent.

### event [#event]

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `event_name` | string | Yes | Up to 255 characters, like `signup`. |
| `url` | string | Yes | Page the event happened on. |
| `event_data` | object | No | Properties, stored as JSON. |
| `session_id` | string | No | From `getSessionId()`. |

### engagement [#engagement]

Scroll depth and engaged time for one page view. The tracker sends it when the visitor leaves the page, with cumulative values. Supapulse stores them on the latest pageview with the same `session_id` and path from the last 24 hours, keeping the highest values. It never counts toward usage.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | Yes | URL of the page view. Its path must match the pageview. |
| `session_id` | string | Yes | The session ID sent with the pageview. |
| `scroll_depth` | integer | Yes | Deepest point reached, `0` to `100` percent. |
| `engaged_seconds` | integer | Yes | Seconds the page was visible and focused, up to `86400`. |

### identify [#identify]

Links a visitor to your customer ID so revenue can be attributed. Works with either token.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `customer_id` | string | Yes | Your user or customer ID. Up to 255 characters. |
| `session_id` | string | No | From `getSessionId()`. Up to 64 characters. |
| `visitor_fingerprint` | string | No | Up to 64 characters. Secret token only; ignored with a public token. |
| `source_customer_id` | string | No | Up to 255 characters. Secret token only. Copies the attribution stored for another customer ID. |

With a public token the server builds the visitor fingerprint from the request's IP address and user agent. Identify events never count toward usage.

### revenue (secret token) [#revenue-secret-token]

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `customer_id` | string | Yes | Your customer key, up to 255 characters. The same ID you pass to `identify()`. |
| `amount` | number | Yes | In major units, like `49.90`. From `0`. Refunds are sent as positive and stored as negative. |
| `transaction_id` | string | Yes | Unique per payment and `event_type` within a site. Repeats are ignored. |
| `event_type` | string | No | `purchase` (default), `subscription`, `renewal`, `upgrade`, `downgrade`, `churn` or `refund`. |
| `currency` | string | No | Three letters. Defaults to `USD`. Not converted. |
| `visitor_fingerprint` | string | No | From `getVisitorFingerprint()`. Up to 64 characters. |
| `session_id` | string | No | From `getSessionId()`. Up to 64 characters. |
| `subscription_id` | string | No | Your subscription reference. |
| `mrr_impact` | number | No | Change in monthly recurring revenue. |
| `is_recurring` | boolean | No | Defaults from `event_type`. |
| `metadata` | object | No | Up to 50 keys. |
| `payment_processor` | string | No | Free text, like `stripe` or `paddle`. Defaults to `stripe`. |
| `occurred_at` | date | No | ISO 8601. Defaults to now. Up to a day in the future. |

Every rule is listed in the [Revenue API reference](https://supapulse.app/docs/revenue/api-reference). Revenue is validated before it's queued, so a bad field returns `422`.

The server builds the visitor hash for pageviews and events from the request's IP address and user agent. For browser traffic, send requests from the browser so those are the visitor's.

## Example [#example]

```bash
curl -X POST https://supapulse.app/api/ingest \
  -H "X-API-Token: sec_9d…41" \
  -H "Content-Type: application/json" \
  -d '{
  "type": "revenue",
  "data": {
    "customer_id": "user_123",
    "amount": 49.90,
    "currency": "USD",
    "transaction_id": "in_1Q8x…",
    "event_type": "renewal",
    "payment_processor": "stripe"
  }
}'
```

```json title="202 Accepted"
{
  "success": true
}
```

`202` means the event passed the token, origin, plan and validation checks and was queued. A worker stores it shortly after. A `transaction_id` that repeats with the same `event_type` is ignored by the worker.

## Other endpoints [#other-endpoints]

### `POST /api/fingerprint` [#post-apifingerprint]

Returns the visitor fingerprint for the caller: the same hash Supapulse stores on their pageviews. Send the token as `X-API-Token` or a `token` body field. `getVisitorFingerprint()` calls it for you.

```json title="200 OK"
{ "visitor_fingerprint": "9b74c9…" }
```

It answers `401`, `403` and `402` like `/api/ingest`, and is never limited by the monthly event quota. It shares the rate limit.

### `POST /api/webhooks/stripe/{publicToken}` [#post-apiwebhooksstripepublictoken]

Receives a site's Stripe events. Stripe calls it, not your code. See the [Stripe guide](https://supapulse.app/docs/guides/stripe) and the [Revenue API reference](https://supapulse.app/docs/revenue/api-reference#stripe-webhook).

## Errors [#errors]

Errors return JSON with an `error` message, or Laravel's validation format for `422`. Retry `429` and `5xx` with backoff; the browser tracker already does.

| Status | Error | What to do |
| --- | --- | --- |
| `401` | `Missing API token` | Send the `X-API-Token` header. |
| `401` | `Invalid token format` | Tokens start with `pub_` or `sec_`. Check for stray spaces. |
| `401` | `Invalid API token` | The token was regenerated or the site was deleted. Copy it again from the site's settings. |
| `402` | `This site's team has no active plan.` with `code: "plan_inactive"` | Start a plan or trial for the site's team. |
| `402` | `This team reached its monthly event limit.` with `code: "quota_exceeded"` | Wait for the monthly reset or upgrade. Never returned for `revenue`. |
| `403` | `Origin not allowed` | The request came from a domain that doesn't match the site. Fix the domain in the site's settings. |
| `422` | Validation failed | A field broke a rule, or `type` isn't allowed for this token. The response lists each field. |
| `429` | Too many requests | Wait for the `Retry-After` header, then retry. |

Details and fixes are in [Error codes](https://supapulse.app/docs/api/errors).

## SDK compatibility [#sdk-compatibility]

`@supapulse/core` sends `pageview`, `event` and `identify`. `@supapulse/revenue` sends `revenue` and works without a fingerprint or session ID.
