REST API
The ingest endpoint the SDKs use. Call it directly from any language or platform.
POST https://supapulse.app/api/ingestJSON in, JSON out, rate limited to 2,000 requests per hour per IP address. To read your analytics back out, use the stats API or the MCP server.
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
Every request has a type and a data object. The fields of data depend on the type. Fields not listed are ignored.
{
"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
| 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
| 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
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
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)
| 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. 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
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"
}
}'{
"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
POST /api/fingerprint
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.
{ "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}
Receives a site's Stripe events. Stripe calls it, not your code. See the Stripe guide and the Revenue API reference.
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.
SDK compatibility
@supapulse/core sends pageview, event and identify. @supapulse/revenue sends revenue and works without a fingerprint or session ID.