Event feed & parity
Consumers read the global append-only feed, page it with a cursor, and reconcile against what they emitted. Delivery is at-least-once and a late merge can repoint an event's customerId, so consumers key on eventId and make processing idempotent. All read endpoints take the read scope (global) or read:own (own source only).
The feed
GET /v1/events/feed serves the append-only stream in two modes:
- Ascending (default), by ingestion id. Pass
afterId(exclusive) and persist the returnednextAfterIdas your own cursor. There is no server-side consumer offset. - Descending, by occurred time. Pass
order=descfor newest-first browsing. Page older by passing bothnextBeforeOccurredAtandnextBeforeIdback asbeforeOccurredAtandbeforeId(a keyset cursor).
Query params: afterId (int >= 0, default 0), beforeId (int >= 1), beforeOccurredAt (ISO-8601), order (asc or desc, default asc), limit (1..1000, default 200), types (csv), source (string).
A read:own key is confined to its own source: source is forced to the caller's callerId, and a different requested source returns 403.
Key your processing on eventId, not customerId. A late merge repoints historical events' customerId onto the surviving customer; the id and eventId are stable, the customerId can change. See Identity resolution.
Request
curl "https://c360-api.lioncapventures.com/v1/events/feed?afterId=0&limit=200&types=order.paid,billpay.completed" \
-H "X-API-Key: $C360_API_KEY"
Response (ascending)
{
"success": true,
"data": {
"events": [
{
"id": 1234,
"publicId": "evt_9f2c...",
"source": "smilemall",
"eventId": "mall-order-paid-ord_abc123",
"type": "order.paid",
"schemaVersion": 1,
"occurredAt": "2026-08-10T09:15:00.000Z",
"receivedAt": "2026-08-10T09:15:02.114Z",
"customerId": "cus_a1b2c3",
"zbIdSubject": "b1e5c8a0-...",
"identity": { "platform": "smilemall", "userId": "42" },
"payload": { "orderId": "ord_abc123", "subtotal": "40.00" }
}
],
"nextAfterId": 1234,
"hasMore": true,
"count": 200
}
}
Response (order=desc)
{
"success": true,
"data": {
"events": [ /* ... newest first by occurredAt ... */ ],
"nextBeforeOccurredAt": "2026-08-10T08:59:00.000Z",
"nextBeforeId": 1180,
"hasMore": true,
"count": 200
}
}
A single event
GET /v1/events/{publicId} returns one event by its publicId. An unknown id returns 404 with { "detail": "Event not found" }.
For a read:own key, an event outside the caller's own source also returns 404 (not 403), so a partner never even learns another source's events exist.
Request
curl https://c360-api.lioncapventures.com/v1/events/evt_9f2c... \
-H "X-API-Key: $C360_API_KEY"
Counts
GET /v1/events/counts returns event counts by type in one aggregate, the cheap cross-check for "did my N events land". Query params: source, types (csv), fromOccurredAt, toOccurredAt (an inclusive ISO-8601 window).
A read:own key is confined to its own source, and a different requested source returns 403.
Request
curl "https://c360-api.lioncapventures.com/v1/events/counts?fromOccurredAt=2026-08-10T00:00:00Z" \
-H "X-API-Key: $C360_API_KEY"
Response (200)
{
"success": true,
"data": {
"counts": { "order.paid": 120, "billpay.completed": 87 },
"total": 207
}
}
Parity and repair
Producers reconcile what they emitted against what Customer 360 stored, then re-emit any gaps. Because ingest is idempotent on (source, eventId), this is always safe: a re-emit of an already-stored event is a duplicate no-op.
POST /v1/events/exists probes a batch of producer eventIds and splits them into present and missing. eventIds is 1 to 2000 ids, each 1 to 120 chars. source defaults to the caller's callerId, so a producer only ever reconciles its own dedupe scope. Re-emit the missing ids with the same stable eventIds.
A read:own key ignores any source in the body and always checks its own source. Pair exists with counts above for a complete "did everything land" reconciliation.
Request
curl -X POST https://c360-api.lioncapventures.com/v1/events/exists \
-H "X-API-Key: $C360_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"eventIds": [
"mall-order-paid-ord_a",
"mall-order-paid-ord_b"
]
}'
Response (200)
{
"success": true,
"data": {
"present": ["mall-order-paid-ord_a"],
"missing": ["mall-order-paid-ord_b"]
}
}
Consumers must treat replays as the duplicates they are. After a producer's repair pass, or your own restart, you may see an eventId you already processed; no-op on it.