Direct HTTP & cURL

The ingest API contract — use ChurnWarn with no SDK at all.

The SDKs are thin wrappers over two endpoints. If your language has no SDK, or you prefer to own the transport, call them directly.

Authentication

Send one of these headers on every request:

HeaderUse for
X-Api-Key: <key>Server-side ingestion. From Settings → API keys.
Authorization: Bearer <jwt>Browser / short-lived sessions.

The tenant is resolved from the key (or the JWT's tenant_id claim). Pass tenantId in the body only to override it with a tenant you're allowed to access.

Record one event

POST/api/events

Body fields:

FieldTypeRequiredNotes
externalAccountIdstringAccount id in your system (≤ 100 chars).
eventTypestringCanonical metric, dotted raw name, or custom string (≤ 100 chars).
payloadstringA JSON string of context. Defaults to "{}".
sourcestringFree-form origin label, e.g. backend.
occurredAtstring (ISO-8601)Event time; defaults to receipt time.
tenantIdUUIDOverride the auth-resolved tenant.
idempotencyKeystringDe-dupes retries; auto-generated if omitted.
payload is a string, not an object

On the wire, payload is a JSON-encoded string — note the escaped quotes below. The SDKs serialize objects for you.

curl -X POST https://your-gateway.example.com/api/events \
-H "X-Api-Key: $CHURNWARN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "externalAccountId": "acct-123",
  "eventType": "feature_used",
  "source": "backend",
  "occurredAt": "2026-06-24T10:00:00Z",
  "payload": "{\"feature\":\"export_csv\"}"
}'

Record a batch

POST/api/events/batch

Wrap up to 500 events in an events array. An optional top-level tenantId applies to the whole batch; if events carry their own tenantId, they must all match it.

curl -X POST https://your-gateway.example.com/api/events/batch \
-H "X-Api-Key: $CHURNWARN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "tenantId": "00000000-0000-0000-0000-000000000000",
  "events": [
    {"externalAccountId": "acct-1", "eventType": "login", "payload": "{}"},
    {"externalAccountId": "acct-2", "eventType": "feature_used", "payload": "{\"feature\":\"reports\"}"}
  ]
}'

A 202 Accepted means the batch was queued. The response can still include per-row error strings for individual events that failed validation.

Enrich account data

Events capture behavior; some churn signals are facts about the account — its plan, lifetime value, or stateful flags like direct_deposit, push_opt_in, kyc_completed. Upsert those by external id.

PUT/api/accounts/{externalId}

Every field is optional — only the ones you send are written. Identity, classification, and attributes are always applied; the commercial block yields to a connected billing provider (so manual edits never fight webhooks).

FieldTypeNotes
name, emailstringIdentity.
kindstringcompany (default) or person (B2C consumer).
businessTypestringPer-account override of the project's business type.
monetaryValuenumberHeadline money figure; meaning set by valueBasis.
valueBasisstringmrr · arr · ltv · balance · gmv · none.
currencystringISO code, e.g. USD.
planKey, lifecycleStagestringCurrent plan / free-form lifecycle stage.
renewalAtstring (ISO-8601)Contract renewal date, where the model has one.
statusstringactive · atrisk · churned.
attributesobjectMerged into the account's fact bag; a null value removes a key.
Billing owns the commercial block

Once a billing provider (Stripe, Zarinpal) has projected onto an account, monetaryValue, valueBasis, currency, planKey and renewalAt are owned by billing and manual values for them are ignored. attributes and identity stay yours.

curl -X PUT https://your-gateway.example.com/api/accounts/acct-123 \
-H "X-Api-Key: $CHURNWARN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "kind": "person",
  "valueBasis": "ltv",
  "monetaryValue": 480,
  "attributes": { "direct_deposit": true, "segment": "vip" }
}'

Payload fields for money & quantity signals

Monetary and quantity components (sum_payload, avg_payload) read a numeric field from the event payload — value by default. Send the amount under a consistent key so those components have something to total:

SignalPayloadRead by
order_placed{"value": 49.90}RFM monetary (sum_payload)
transaction{"value": 120, "side": "buyer"}GMV (sum_payload)
consumption_units{"value": 1500}usage volume (sum_payload)
Slow-changing facts vs. metrics

Put metrics (things you count or sum over time) in events. Put slow-changing facts and flags (a balance, an opt-in) in attributes — the bag is for state, not a time series.

Responses & retries

StatusMeaning
202 AcceptedEvent(s) queued for processing.
400 Bad RequestMalformed body or field over a length limit.
401 / 403Missing/invalid key, or tenant not permitted.
429Rate limited — back off and retry.

Retries are safe: set a stable idempotencyKey per logical event so a retried request is de-duplicated server-side.

Next