Events

Producers post events; Customer 360 resolves each one into the identity graph and appends it to a single global stream. This page covers the ingest endpoint, the envelope every event shares, the identity block, the money convention, idempotency, and the type registry the service validates against.


POST/v1/events

Ingest a batch

POST /v1/events takes a batch of 1 to 500 envelopes as { "events": [ ... ] }. The request returns 200 when the request shape is valid; each event's fate is reported inline, so one bad or racing event never fails the batch. Each event is resolved and inserted in its own savepoint.

Every result carries a status of accepted, duplicate, or invalid. An invalid result also carries an error string explaining why (an unknown type, a missing required field, or a money field that does not parse). The response tallies accepted, duplicates, and invalid counts.

Requires the ingest scope. The event's source is stamped from the authenticated key's callerId, never from the body.

Request

POST
/v1/events
curl -X POST https://c360-api.lioncapventures.com/v1/events \
  -H "X-API-Key: $C360_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "eventId": "mall-order-paid-ord_abc123",
        "type": "order.paid",
        "schemaVersion": 1,
        "occurredAt": "2026-08-10T09:15:00.000Z",
        "identity": { "platform": "smilemall", "userId": "42" },
        "payload": {
          "orderId": "ord_abc123",
          "subtotal": "40.00",
          "total": "44.00",
          "currency": "USD"
        }
      }
    ]
  }'

Response (200)

{
  "success": true,
  "data": {
    "results": [
      { "eventId": "mall-order-paid-ord_abc123", "status": "accepted" }
    ],
    "accepted": 1,
    "duplicates": 0,
    "invalid": 0
  }
}

A batch with a bad event

{
  "success": true,
  "data": {
    "results": [
      { "eventId": "ok-1", "status": "accepted" },
      { "eventId": "bad-2", "status": "invalid",
        "error": "missing required payload field 'currency'" }
    ],
    "accepted": 1,
    "duplicates": 0,
    "invalid": 1
  }
}

The envelope

Every event is a JSON object with the same top-level shape. source is not part of the envelope: the service stamps it from the authenticated key.

  • Name
    eventId
    Type
    string, 1..120
    Description

    Producer-stable. The idempotency key together with the caller-stamped source. Reposting the same eventId from the same source is a safe no-op.

  • Name
    type
    Type
    string, 1..60
    Description

    Must be in the type registry below. An unknown type marks the event invalid.

  • Name
    schemaVersion
    Type
    integer
    Description

    Optional, defaults to 1. Bump it when a payload shape changes.

  • Name
    occurredAt
    Type
    ISO-8601 datetime
    Description

    The real-world time, UTC with a trailing Z. Used for feed ordering and windows.

  • Name
    identity
    Type
    object
    Description

    The subject of the event. See the identity block below.

  • Name
    payload
    Type
    object
    Description

    Optional, defaults to {}. Type-specific fields. Extra fields are always allowed and stored verbatim.

Event envelope

{
  "eventId": "qupa-loan-disbursed-APP-9931",
  "type": "loan.disbursed",
  "schemaVersion": 1,
  "occurredAt": "2026-08-10T09:15:00.000Z",
  "identity": {
    "platform": "qupa",
    "phone": "+263771234567",
    "nationalId": "63-1234567A00"
  },
  "payload": {
    "applicationId": "APP-9931",
    "amount": "250.00",
    "currency": "USD"
  }
}

The identity block

platform is required and must exist in the platforms registry (smilemall, qupa, zbid, notification-hub, product-intel, smilerewards). Everything else is optional; supply as much as you know.

  • Name
    platform
    Type
    string, required
    Description

    The channel producing the event.

  • Name
    userId
    Type
    string
    Description

    Your own user id on that platform, the strongest per-platform key.

  • Name
    zbIdSubject
    Type
    string
    Description

    The ZB ID JWT sub UUID, the cross-channel canonical key. Include it whenever you have it, even alongside a userId: this is what unifies the customer across channels.

  • Name
    phone
    Type
    string
    Description

    Normalized to E.164, Zimbabwe-aware (0771234567 becomes +263771234567).

  • Name
    email
    Type
    string
    Description

    Normalized to lower-case and trimmed.

  • Name
    nationalId
    Type
    string
    Description

    National id (Qupa loans key on this plus phone).

  • Name
    sessionId
    Type
    string
    Description

    Anonymous session for guest browsing before login.

  • Name
    displayName
    Type
    string
    Description

    Best-known name.

  • Name
    isGuest
    Type
    boolean
    Description

    true for guest checkout with no user row on your side.


Money convention

Money values are decimal strings ("12.50", "0.00"), never floats. If your system stores cents or integers, divide by 100 before sending (1250 becomes "12.50").

This service computes no money. It validates that money fields parse as Decimal >= 0 and stores them verbatim. It never derives a fee, tax, commission, discount, or total. The read-time trait engine only sums what you stored. A money field that does not parse marks the event invalid.

Money as decimal strings

{
  "payload": {
    "orderId": "ord_abc123",
    "subtotal": "40.00",
    "total": "44.00",
    "currency": "USD"
  }
}

Idempotency

Events dedupe on (source, eventId). Choose an eventId that is stable for the real-world fact, so a retry or a re-emit after a crash is a safe no-op that comes back duplicate. Never mint a random eventId per attempt; that defeats dedupe.

Because ingest is idempotent, reconciliation is always safe: a producer can re-emit missing events with the same stable eventIds and each re-emit is a duplicate. See Event feed & parity for the reconciliation flow.

Stable eventId recipes

mall-order-paid-<orderPublicId>
mall-payment-completed-<paymentPublicId>
qupa-loan-disbursed-<applicationId>
mall-session-<sessionId>-started

Event type registry

Ingest validates the type and its required payload fields. An unknown type, a missing required field, or a money field that does not parse as Decimal >= 0 marks that event invalid (the rest of the batch still processes). Extra payload fields are always allowed and stored verbatim.

The registry below is the complete set of types the service accepts, taken directly from the service. Producers only emit the types relevant to their platform.

Session and browsing

TypeRequired payloadMoney fields
session.startedsessionId-
page.viewedpath-

Identity lifecycle

TypeRequired payloadNotes
identity.registered(none)account created
identity.updatedchangedthe changed fields (for example a zbid attached later)
identity.claimedclaimedType, claimedValuemerges the customer owning that identity into this event's customer

identity.claimed is how a registered user absorbs a prior guest: send it with the user's userId in identity and { "claimedType": "phone", "claimedValue": "+263771234567" } in the payload. See Identity resolution.

Orders

TypeRequired payloadMoney fieldsNotes
order.createdorderId, total, currencytotaloptional merchantId (merchant insight) plus an optional items line array
order.status_changedorderId, status-
order.paidorderId, subtotal, total, currencysubtotal, totalsubtotal is the loyalty earn basis; optional merchantId

An optional items array on order.created unlocks the mallProduct dimension of Insights. Each entry needs productId; name, quantity, and a decimal-string unit price are recommended (the line money is price * quantity).

Payments

TypeRequired payloadMoney fields
payment.completedpaymentId, amount, currencyamount
payment.failedpaymentId-

Bill payments

TypeRequired payloadMoney fieldsNotes
billpay.initiatedtransactionId, amount, total, currency, channelamount, totalchannel for example zesa, dstv
billpay.completedtransactionId, amount, total, currency, channelamount, total
billpay.failedtransactionId-

Tickets

TypeRequired payloadNotes
ticket.purchasedorderId, eventId, quantityhere the payload eventId is the show's id, not the envelope eventId

Loans

TypeRequired payloadMoney fields
loan.appliedapplicationId, amountRequested, currencyamountRequested
loan.status_changedapplicationId, status-
loan.disbursedapplicationId, amount, currencyamount
loan.repaymentapplicationId, amount, currency, statusamount

Messaging

TypeRequired payloadNotes
notification.sentmessageId, channelset identity.platform to the recipient's platform when known
notification.deliveredmessageId, channel, status

Consent

TypeRequired payloadNotes
consent.updatedpurpose, statusupserts the consent state; status is granted or revoked; an older occurredAt than the stored decision is ignored

Was this page helpful?