Authentication & scopes
Customer 360 is a service-to-service API. There is no user login, no OAuth handshake, and no bearer token to refresh. Every request carries a single X-API-Key header, and each calling service holds its own key. A key is scoped, rate limited, and tied to a callerId that identifies the service and stamps the source on every event it ingests.
The X-API-Key header
Send your key in the X-API-Key request header on every call. Keys are prefixed c360_. A missing header returns 401 with Missing X-API-Key header; a header that does not match any active key returns 401 with Invalid API key.
Each key is bound to a callerId, the service that owns it. That callerId is the only source of an event's source field: it is stamped from the key, never read from the request body, so a producer can never spoof another producer's source or dedupe scope.
Authenticated request
curl https://c360-api.lioncapventures.com/v1/events/counts \
-H "X-API-Key: c360_QmFzZTY0VXJsU2FmZVJhbmRvbUtleQ"
How keys are stored
When a key is minted, the raw value is returned to the admin exactly once. Only its SHA-256 hash is stored, alongside a short display prefix and metadata. There is no way to recover a lost key; the admin revokes it and mints a new one.
On each request the incoming header is SHA-256 hashed and matched against the stored hash, so the plaintext key is never written to disk or logs by the service. Store your key in your own project's Secret Manager as C360_API_KEY; cross-project secret references do not work, so each consumer keeps its own copy.
The plaintext c360_ key is shown to the admin only at mint time. Capture it immediately and store it in your Secret Manager. If you lose it, the key must be revoked and re-minted.
What the store keeps
{
"publicId": "key_3f9a2c7b1e04",
"callerId": "qupa",
"keyPrefix": "c360_QmFzZT",
"scopes": "ingest,read:own",
"rateLimitPerMinute": 120,
"isActive": true
}
Scopes
Each key carries one or more scopes as a comma-separated string. A scope gates which endpoints the key may call. A call whose key lacks the required scope returns 403 with Insufficient permissions. Required scope: <scope>.
- Name
ingest- Description
Emit events. Required for
POST /v1/events. This is the producer scope.
- Name
read- Description
Read the GLOBAL cross-source event stream: the feed,
counts,exists, a single event, and the cross-sourceinsightsaggregates. Give this to internal consumers that need the whole stream.
- Name
read:own- Description
Read the same feed,
counts,exists, and single event, but CONFINED to the caller's ownsource. This is the partner-safe scope: a producer can read back only its own events. It cannot read cross-source insights.
- Name
profile- Description
Read unified customer profiles and a customer's events, and write consent decisions (
GET /v1/profiles/*,PUT /v1/profiles/{publicId}/consents).
- Name
loyalty- Description
Resolve and record a customer's SmileRewards member id (
POST /v1/loyalty/members/resolve,GET /v1/loyalty/members).
Scopes are combined on a single key. A producer that ingests and reads its own events back holds ingest,read:own; a consumer that reads the global feed holds read.
Scope by endpoint group
| Endpoint group | Scope |
|---|---|
POST /v1/events | ingest |
| Feed, counts, exists, single event | read or read:own |
GET /v1/insights/* | read |
GET /v1/profiles/*, consent write | profile |
| Loyalty member resolve + read | loyalty |
A read:own key can ingest and read back its own events without ever seeing another producer's stream. It is the scope to hand to an external integrator.
Global vs own-source reads
Reading the event stream needs one of two scopes, and they behave differently:
read(global) sees the whole cross-source stream.sourceis a free filter, and the cross-source insights aggregates are available.read:own(own source) sees only the caller's ownsource. Onfeedandcounts,sourceis forced to the caller's owncallerId, and asking for a differentsourcereturns403. Onexists, anysourcein the body is ignored and the check always runs against the caller's own source. A single event outside the caller's source returns404, so a partner never even learns it exists. Insights are denied with403because they are cross-source aggregates.
See Event feed & parity for the read contract in full.
read:own asking for another source (403)
{
"detail": "A read:own key may only read its own source."
}
read:own on insights (403)
{
"detail": "Insights are cross-source aggregates and are not available to a read:own key. A global `read` scope is required."
}
Auth errors
Authentication and authorization failures use FastAPI's detail shape (a plain { "detail": "..." } object), not the { success, data } envelope that successful responses use. The status code tells you what went wrong.
- Name
401 Unauthorized- Type
- Missing X-API-Key header
- Description
No
X-API-Keyheader was supplied.
- Name
401 Unauthorized- Type
- Invalid API key
- Description
The key does not match any active key.
- Name
403 Forbidden- Type
- Insufficient permissions
- Description
The key is valid but lacks the scope the endpoint requires.
- Name
429 Too Many Requests- Type
- Rate limit exceeded
- Description
The key exceeded its per-minute limit. Carries
X-RateLimit-LimitandX-RateLimit-Remainingheaders.
Auth errors return { "detail": "..." }. Business rejections from the service return a structured { success: false, error: { code, message } } envelope. See the Errors page.