HTTP API
The ingest and flow endpoints, request by request.
#Base URL and authentication
Every request goes to your worker URL (shown in the dashboard under Settings › API). The base URL in production is https://ingest.pushlane.io by default, but changes once you add a custom domain.
All write-key-gated endpoints require a publishable write-key in one of two forms:
POST /v1/events
Authorization: Bearer lpk_live_xxxxxxxxxxxxxxxx
Content-Type: application/jsonThe key format is lpk_live_…. The worker derives your tenant server-side from the key — the tenantId you pass in the body is overridden by the key's tenant. You can also use the header X-Pushlane-Key: lpk_live_… instead of Authorization.
#POST /v1/events
Ingest a single event. The worker normalises it (generating an eventId and traceparent if absent), resolves the identity, and enqueues it for async processing. Returns 202 Accepted immediately — processing is asynchronous.
occurredAt is required and must be epoch milliseconds. The SDK always sends it; direct HTTP callers must supply it.{
"tenantId": "YOUR_TENANT_UUID", // overridden server-side from the key
"externalId": "user_42", // your stable user id
"name": "paywall_viewed",
"occurredAt": 1751400000000, // epoch ms — required
"properties": {
"placement": "onboarding",
"paywall_id": "main"
}
}// HTTP 202 Accepted
{
"eventId": "01939e7c-aaaa-7000-bbbb-000000000001",
"surrogate": 12345,
"traceparent":"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"shard": 2
}| Field | Type | Required | Notes |
|---|---|---|---|
| tenantId | UUID string | Yes* | Overridden by the key's tenant server-side. |
| externalId | string (min 1) | Yes | Your stable user identifier. |
| name | string (min 1) | Yes | Snake_case event name, e.g. paywall_viewed. |
| occurredAt | number (epoch ms) | Yes | When the event happened on-device. |
| properties | object | No | Key/value pairs; values may be string, number, boolean, null, or an array of those. |
| eventId | UUID string | No | Auto-generated if absent. Send your own for idempotency. |
| traceparent | W3C string | No | Auto-generated if absent or invalid. |
#POST /v1/register
Register or refresh a device push token. One subscription per device token per tenant. Calling this again with the same token updates the record (app version, OS version, last-seen timestamp).
The pushEnvironment field is critical for correct APNs routing. The iOS SDK auto-detects it from the provisioning profile — never derive it from #if DEBUG. If you are calling this endpoint directly, match what Apple's Certificates, Identifiers & Profiles shows for the token's environment.
{
"tenantId": "YOUR_TENANT_UUID",
"externalId": "user_42",
"deviceToken": "a1b2c3d4...", // hex-encoded APNs token
"pushEnvironment":"sandbox", // "sandbox" | "production"
"appVersion": "2.1.0", // optional, recommended
"osVersion": "17.5.0" // optional, recommended
}// HTTP 200 OK
{
"subscriptionId": "550e8400-e29b-41d4-a716-446655440000",
"canonicalId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"surrogate": 12345, // present only on first registration
"updated": false
}#POST /v1/identify
Resolve an externalId to its Pushlane canonical identity (stable UUID + dense surrogate integer). The canonical identity is created on first call. Useful for verifying a user exists in Pushlane before activating a flow.
{
"tenantId": "YOUR_TENANT_UUID",
"externalId":"user_42"
}// HTTP 200 OK
{
"tenantId": "YOUR_TENANT_UUID",
"externalId": "user_42",
"canonicalId":"3fa85f64-5717-4562-b3fc-2c963f66afa6",
"surrogate": 12345,
"firstSeen": false
}firstSeen is true when the identity was created by this call (i.e. this is the first event or register for this user).
#POST /v1/consent
Record an explicit marketing consent decision for a user. Pushlane uses an opt-out model: a registered device (push notification permission granted) is treated as opted-in by default. This endpoint records the only state the engine treats as authoritative to suppress: an explicit opt_out — and the way back in (opt_in).
An opt_out row is always honoured — a user who has explicitly unsubscribed will never receive a marketing push until they opt back in.
/v1/register or identified via /v1/identify) before you can record consent. Consent for an unknown externalId returns 404 unknown_user. No phantom user is created.{
"externalId": "user_42",
"action": "opt_out", // "opt_out" | "opt_in"
"category": "marketing" // optional — defaults to "marketing"
}// HTTP 200 OK (opt_out recorded)
{ "ok": true, "optedIn": false }
// HTTP 200 OK (opt_in recorded)
{ "ok": true, "optedIn": true }| Field | Type | Required | Notes |
|---|---|---|---|
| externalId | string (min 1) | Yes | Your stable user identifier. Must already exist in Pushlane. |
| action | "opt_in" | "opt_out" | Yes | opt_out suppresses all future marketing pushes. opt_in re-enables them. |
| category | string | No | Consent category. Defaults to "marketing". Other categories are ignored by the flow engine today. |
opt_in (or call Pushlane.setMarketingConsent(true)) after the user consents.From the SDK: Pushlane.setMarketingConsent(false) calls this endpoint with action: "opt_out"; Pushlane.setMarketingConsent(true) calls it with action: "opt_in". The dashboard Preference Center also writes consent rows via this endpoint.
#POST /v1/flows/activate
Compile and activate a flow from a FlowIR document. Activated flows are stored in Supabase and evaluated on every ingested event for the tenant. This endpoint is called by the Pushlane dashboard when you click Activate on the canvas — you rarely need to call it directly.
A successful activation returns 200 with { flowId, version, warnings }. A schema validation failure returns 422 with an { errors, warnings } object describing each problem.
#GET /v1/catalogue
Returns the tenant's typed event and property catalogue — every event name and property that has been declared or auto-discovered from ingested events. Requires a write-key.
curl https://<YOUR_WORKER>/v1/catalogue \
-H "Authorization: Bearer lpk_live_…"The response shape is { tenantId, events: [...], properties: [...] }. Each event has name, declared, description, volume30d, and lastSeenAt. Each property has name, kind (event_property | user_attribute), type, and optional eventName.
#GET /v1/install-status
Live proof the SDK is sending events for the tenant. Reports distinct event names, total event count, last event time, and connected device counts from ClickHouse and Supabase respectively. Each source reports its own ok/error so a partial outage still answers honestly. Requires a write-key.
curl https://<YOUR_WORKER>/v1/install-status \
-H "Authorization: Bearer lpk_live_…"sdkDetected is true once any event or device registration exists for the tenant.
#GET /healthz
Health check. No authentication required. Returns 200 when the worker is up.
// HTTP 200 OK
{ "ok": true, "service": "loop-ingest" }#Error reference
All error responses are JSON with an error string key. Some include an issues array (422); server-side failures return a generic error plus a requestId you can quote to support (raw internal detail is never exposed).
| HTTP | error | Cause | Fix |
|---|---|---|---|
401 | write_key_required | No key presented and auth enforcement is on. | Add Authorization header. |
401 | invalid_write_key | Key does not match any active row. | Check the key — it may be revoked. |
502 | write_key_verification_failed | Transient Supabase lookup error. | Retry; not a bad key. |
400 | invalid_json | Body could not be parsed. | Check Content-Type and body encoding. |
422 | validation_failed | Body failed schema validation. | Check the issues array in the response. |
400 | tenantId_and_externalId_required | /v1/identify missing a field. | Supply both fields. |
400 | tenantId_externalId_deviceToken_required | /v1/register missing a field. | Supply all three fields. |
400 | pushEnvironment_must_be_sandbox_or_production | Invalid pushEnvironment value. | Use "sandbox" or "production". |
502 | identity_resolve_failed | Could not resolve externalId → canonical. | Transient; retry. |
502 | enqueue_failed | Queue write failed. | Transient; retry. |
404 | unknown_user | /v1/consent: externalId not yet registered or identified. | Call /v1/register or /v1/identify first. |
422 | invalid_consent_body | /v1/consent: body failed schema validation. | Supply externalId (string) and action ('opt_in' | 'opt_out'). |
502 | consent_failed | /v1/consent: transient Supabase error. | Retry; not a bad key or body. |
Next: iOS SDK reference →