Ingestion
Update people profile
POST /api/v1/people — set traits (email, plan, role, etc.) on a user. Async write to the People store, no identity change.
The people endpoint updates a user's profile traits without changing their identity. Use it for setting display traits (name, email, avatar) and custom properties (plan, role, tenant_id) that should appear on the user's profile in the dashboard.
For identity stitching (anon → known), use /api/v1/alias — different responsibility.
Authentication
Required header: x-api-key: sk_live_… or x-api-key: sk_test_…. See Authentication.
Request body
distinct_idstringRequiredpropertiesobjecttimestampstring (ISO8601)Example request
curl -X POST https://api.sankofa.dev/api/v1/people \
-H "Content-Type: application/json" \
-H "x-api-key: sk_live_..." \
-d '{
"distinct_id": "user_123",
"properties": {
"email": "ada@example.com",
"name": "Ada Lovelace",
"plan": "pro",
"signup_date": "2025-01-12",
"tenant_id": "acme"
}
}'Response
200 OK on success — the response body is empty.
For garbage distinct_id, returns 202 Accepted with {"ok": true, "status": "discarded"}. See Errors.
Merge semantics
people is a partial update — only the keys in your properties object are touched. Existing keys not mentioned are preserved.
To delete a trait, set it to null:
{
"distinct_id": "user_123",
"properties": {
"deprecated_trait": null
}
}The dashboard's People view treats null as "remove this trait."
Reserved trait names
Some trait names get special treatment in the dashboard:
| Key | Behavior |
|---|---|
name | Shown in the People-view header next to the avatar. |
email | Surfaced in the contact strip; used as the default search match. |
avatar | Image URL rendered in the avatar circle. |
phone | Surfaced in the contact strip. |
Everything else is custom and shown in the "Properties" tab on the user's profile.
Identity stitching
people does not change the active user. If you set a trait on user_123, that's the user the trait is associated with — period. Calling people does not implicitly alias an anonymous ID onto a known one.
To stitch anonymous-to-known identity, call /api/v1/alias (or use the SDK's identify(...) which calls alias under the hood).
When to send people
Three common patterns:
On sign-up
Right after the user creates an account, call
peoplewithemail,name,plan, and any other static traits.On profile update
When the user edits their profile, call
peoplewith just the changed traits. Don't re-send everything.On state transition
When their plan upgrades, their role changes, their cohort flips — call
peoplewith the new value. The dashboard will reflect the change immediately.
For server-side workloads (cron jobs, billing webhooks, CRM syncs), call people directly — the official SDK's setPerson / peopleSet is just a thin wrapper around this endpoint.
Validation
Same checks as track:
- Auth (
x-api-key) - Origin / IP allowlist
- JSON parse
- Required
distinct_idfield - Garbage-ID heuristic (returns
202if matched) - Queue + return
Async write semantics
Same as track — the handler queues the update and returns immediately. A background worker (startPersonWorker()) batches updates and writes them to the People store. Latency is sub-millisecond response; reads see the new traits within ~2 seconds.
If two people updates arrive close together for the same user, the one with the later timestamp wins. If both timestamps are the same (or omitted, defaulting to server time), the order is determined by which the worker picks up first — typically a few hundred microseconds apart, but not strictly defined.