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:
| Header | Use 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
/api/eventsBody fields:
| Field | Type | Required | Notes |
|---|---|---|---|
externalAccountId | string | ✓ | Account id in your system (≤ 100 chars). |
eventType | string | ✓ | Canonical metric, dotted raw name, or custom string (≤ 100 chars). |
payload | string | — | A JSON string of context. Defaults to "{}". |
source | string | — | Free-form origin label, e.g. backend. |
occurredAt | string (ISO-8601) | — | Event time; defaults to receipt time. |
tenantId | UUID | — | Override the auth-resolved tenant. |
idempotencyKey | string | — | De-dupes retries; auto-generated if omitted. |
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
/api/events/batchWrap 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.
/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).
| Field | Type | Notes |
|---|---|---|
name, email | string | Identity. |
kind | string | company (default) or person (B2C consumer). |
businessType | string | Per-account override of the project's business type. |
monetaryValue | number | Headline money figure; meaning set by valueBasis. |
valueBasis | string | mrr · arr · ltv · balance · gmv · none. |
currency | string | ISO code, e.g. USD. |
planKey, lifecycleStage | string | Current plan / free-form lifecycle stage. |
renewalAt | string (ISO-8601) | Contract renewal date, where the model has one. |
status | string | active · atrisk · churned. |
attributes | object | Merged into the account's fact bag; a null value removes a key. |
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:
| Signal | Payload | Read 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) |
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
| Status | Meaning |
|---|---|
202 Accepted | Event(s) queued for processing. |
400 Bad Request | Malformed body or field over a length limit. |
401 / 403 | Missing/invalid key, or tenant not permitted. |
429 | Rate 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
- Security & data masking — auth depth, CORS, redaction.
- SDK overview — let an SDK handle batching and retries for you.