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.

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-source insights aggregates. 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 own source. 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 groupScope
POST /v1/eventsingest
Feed, counts, exists, single eventread or read:own
GET /v1/insights/*read
GET /v1/profiles/*, consent writeprofile
Loyalty member resolve + readloyalty

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. source is a free filter, and the cross-source insights aggregates are available.
  • read:own (own source) sees only the caller's own source. On feed and counts, source is forced to the caller's own callerId, and asking for a different source returns 403. On exists, any source in the body is ignored and the check always runs against the caller's own source. A single event outside the caller's source returns 404, so a partner never even learns it exists. Insights are denied with 403 because 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-Key header 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-Limit and X-RateLimit-Remaining headers.

Was this page helpful?