Pushlanedocs
Documentation menu

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:

http
POST /v1/events
Authorization: Bearer lpk_live_xxxxxxxxxxxxxxxx
Content-Type: application/json

The 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.

Heads up
Write-keys are publishable (they ship inside your app), not secret. They identify a tenant and scope ingestion to it. They cannot read or modify tenant settings.

#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.

Note
occurredAt is required and must be epoch milliseconds. The SDK always sends it; direct HTTP callers must supply it.
json
{
  "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"
  }
}
json
// HTTP 202 Accepted
{
  "eventId":    "01939e7c-aaaa-7000-bbbb-000000000001",
  "surrogate":  12345,
  "traceparent":"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
  "shard":      2
}
FieldTypeRequiredNotes
tenantIdUUID stringYes*Overridden by the key's tenant server-side.
externalIdstring (min 1)YesYour stable user identifier.
namestring (min 1)YesSnake_case event name, e.g. paywall_viewed.
occurredAtnumber (epoch ms)YesWhen the event happened on-device.
propertiesobjectNoKey/value pairs; values may be string, number, boolean, null, or an array of those.
eventIdUUID stringNoAuto-generated if absent. Send your own for idempotency.
traceparentW3C stringNoAuto-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.

json
{
  "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
}
json
// 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.

json
{
  "tenantId":  "YOUR_TENANT_UUID",
  "externalId":"user_42"
}
json
// 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.

Heads up
The user must already exist in Pushlane (registered via /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.
json
{
  "externalId": "user_42",
  "action":     "opt_out",   // "opt_out" | "opt_in"
  "category":   "marketing"  // optional — defaults to "marketing"
}
json
// HTTP 200 OK  (opt_out recorded)
{ "ok": true, "optedIn": false }

// HTTP 200 OK  (opt_in recorded)
{ "ok": true, "optedIn": true }
FieldTypeRequiredNotes
externalIdstring (min 1)YesYour stable user identifier. Must already exist in Pushlane.
action"opt_in" | "opt_out"Yesopt_out suppresses all future marketing pushes. opt_in re-enables them.
categorystringNoConsent category. Defaults to "marketing". Other categories are ignored by the flow engine today.
Note
EU / GDPR: in certain jurisdictions (EU) an explicit opt-in may be legally required before sending marketing notifications. That is the responsibility of the app. Pushlane provides both mechanisms — opt-out by default and explicit opt-in/opt-out recording. When your audience includes EU users, record an explicit 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.

bash
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.

bash
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.

json
// 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).

HTTPerrorCauseFix
401write_key_requiredNo key presented and auth enforcement is on.Add Authorization header.
401invalid_write_keyKey does not match any active row.Check the key — it may be revoked.
502write_key_verification_failedTransient Supabase lookup error.Retry; not a bad key.
400invalid_jsonBody could not be parsed.Check Content-Type and body encoding.
422validation_failedBody failed schema validation.Check the issues array in the response.
400tenantId_and_externalId_required/v1/identify missing a field.Supply both fields.
400tenantId_externalId_deviceToken_required/v1/register missing a field.Supply all three fields.
400pushEnvironment_must_be_sandbox_or_productionInvalid pushEnvironment value.Use "sandbox" or "production".
502identity_resolve_failedCould not resolve externalId → canonical.Transient; retry.
502enqueue_failedQueue write failed.Transient; retry.
404unknown_user/v1/consent: externalId not yet registered or identified.Call /v1/register or /v1/identify first.
422invalid_consent_body/v1/consent: body failed schema validation.Supply externalId (string) and action ('opt_in' | 'opt_out').
502consent_failed/v1/consent: transient Supabase error.Retry; not a bad key or body.

Next: iOS SDK reference →