Events: how delivery works today
status: written from ADR-062, ADR-006, ADR-007 and the code. The event list is generated below. [FO]
Events say what happened. They never move money.
Built
| Part | State | Flag |
|---|---|---|
| Outbox: events are written in the same database transaction as the change. A publisher sends them on. | [BT] | EVENTS_PUBLISHER (kafka or off, default off) |
| Integration log: one ordered stream per operator, a sequence number with no gaps. | [FO] | INTEGRATION_LOG_ENABLED |
Signed webhooks: batches, retries, subscription suspended after 72 hours of failure. Secrets need WEBHOOK_SECRET_STORE=kv2. | [FO] | EVENT_SUBSCRIPTIONS_ENABLED |
Test event: POST /v1/s2s/event-subscriptions/{id}/test sends one signed neun.predictions.subscription.test event to the subscription. The secret is never returned. Another operator gets 404. A disabled subscription gets 422 SUBSCRIPTION_NOT_ACTIVE. Unsafe addresses are refused. Each subscription has a test budget (429). | [FO] | EVENT_SUBSCRIPTIONS_ENABLED |
Pull feed: GET /v1/s2s/integration-events?after=&limit=&types=&wait= with scope events:read. | [BT] | none |
Pull feed example parameters from the OpenAPI file: after=48212, types=order.completed,sell.failed. A cursor that is too old returns 410 CURSOR_EXPIRED. Delivery is at least once. Remove duplicates by event id.
Not built
| Part | State |
|---|---|
| Kafka topic for operators (delivery mode C) | [NB] Open: N14 |
Bootstrap exports (POST /v1/s2s/exports) | [NB] Open: N14 |
Envelope, order and partition keys
Each event is a CloudEvent 1.0 in JSON. Every event has neunpartitionkey (the data field named in the AsyncAPI x-partition-key), neunresourceversion and neunsequence (no gaps, per operator).
Order holds per partition key. Apply an event only when its neunresourceversion is higher than the one you already hold. Do not order by arrival time.
| Partition key | Events |
|---|---|
orderId | order.accepted, order.cancelled, order.completed, order.expired, order.failed |
marketId | market.resolved, correction.applied |
operatorId | operator.wallet.health_changed |
positionId | position.opened, position.settled, position.voided |
sellId | sell.completed, sell.failed |
settlementLineId | settlement.line_settled |
receivableId | player.receivable.opened, player.receivable.closed |
operationId | wallet.operation.escalated, wallet.operation.finalized |
writeOffId | write_off.requested, write_off.approved |
Exception: correction.applied is keyed by correctionId (ADR-062:95); the internal Kafka key stays marketId (ADR-006:32). Order the events of one correction by neunresourceversion.
order.cancelled and order.expired are defined but not emitted in R1.
Delivery notes
- Webhook body: a JSON array of up to
batchSizeevents (1 to 500), at most 1 MB. Headers:Neun-Delivery-Id,Neun-Subscription-Id,Neun-Event-Count,Neun-First-Sequence,Neun-Last-Sequence,Neun-Signature. - Answer 2xx within 10 s. Otherwise the batch is sent again after 30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, then every 4 h.
Retry-Afteris honoured. After 72 h of failure the subscription is suspended. With STRICT ordering the next batch waits until the open one is acknowledged. - An event over 64 KB is replaced by a placeholder at the same sequence.
- Signature:
Neun-Signature: t=<unix ms>,v1=<hex>. The hex must be lowercase. Uppercase hex is refused. See the signing guide. - Delivery is at least once. Remove duplicates by event
id.
Server SDK helpers (on a branch, not released)
parseEventBatch checks the signature on the raw bytes, then reads the batch. It refuses a body over 1,048,576 bytes and a batch over 500 events. pollEvents reads the pull feed and never follows a redirect, so the token cannot go to another host. Both are in @neungaming/predictions-server on branch feat/v20-r15-serversdk: on branch, not released. See what is released.
Contract file: packages/contracts/asyncapi/events-v1.json (AsyncAPI). The generated list of event types follows.
Event catalogue
status: generated from packages/contracts/asyncapi/events-v1.json (20 types). [FO] Event Connector flags default off. Types with x-status defined-not-emitted have no producer. Dedupe by id; delivery is at least once.
correction.applied
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.correction.applied. Partition key: correctionId.
| field | type | required |
|---|---|---|
changeType | string ["APPLIED"] | yes |
correctionId | string | yes |
marketId | string | yes |
origin | string ["STAFF","SOURCE"] | yes |
state | string | yes |
newResolutionVersion | ? | yes |
market.resolved
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.market.resolved. Partition key: marketId.
| field | type | required |
|---|---|---|
changeType | string ["RESOLVED"] | yes |
marketId | string | yes |
resolutionId | string | yes |
resolutionVersion | integer | yes |
outcome | string | yes |
state | string ["RESOLVED_FINAL","VOIDED"] | yes |
settlementVector | array | yes |
operator.wallet.health_changed
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.operator.wallet.health_changed. Partition key: operatorId.
| field | type | required |
|---|---|---|
changeType | string ["UP","DEGRADED"] | yes |
operatorId | string | yes |
status | string ["UP","DEGRADED"] | yes |
order.accepted
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.order.accepted. Partition key: orderId.
| field | type | required |
|---|---|---|
changeType | string ["ACCEPTED"] | yes |
orderId | string | yes |
quoteId | string | no |
playerLinkId | string | yes |
marketId | string | yes |
outcomeId | string | yes |
state | string | yes |
failureCode | string,null | no |
totalDebit | object | no |
order.cancelled
x-status: defined-not-emitted. CloudEvents type neun.predictions.order.cancelled. Partition key: orderId.
| field | type | required |
|---|---|---|
changeType | string ["CANCELLED"] | yes |
orderId | string | yes |
quoteId | string | no |
playerLinkId | string | yes |
marketId | string | yes |
outcomeId | string | yes |
state | string | yes |
failureCode | string,null | no |
totalDebit | object | no |
order.completed
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.order.completed. Partition key: orderId.
| field | type | required |
|---|---|---|
changeType | string ["COMPLETED"] | yes |
orderId | string | yes |
quoteId | string | no |
playerLinkId | string | yes |
marketId | string | yes |
outcomeId | string | yes |
state | string | yes |
failureCode | string,null | no |
totalDebit | object | no |
order.expired
x-status: defined-not-emitted. CloudEvents type neun.predictions.order.expired. Partition key: orderId.
| field | type | required |
|---|---|---|
changeType | string ["EXPIRED"] | yes |
orderId | string | yes |
quoteId | string | no |
playerLinkId | string | yes |
marketId | string | yes |
outcomeId | string | yes |
state | string | yes |
failureCode | string,null | no |
totalDebit | object | no |
order.failed
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.order.failed. Partition key: orderId.
| field | type | required |
|---|---|---|
changeType | string ["FAILED"] | yes |
orderId | string | yes |
quoteId | string | no |
playerLinkId | string | yes |
marketId | string | yes |
outcomeId | string | yes |
state | string | yes |
failureCode | string,null | no |
totalDebit | object | no |
player.receivable.closed
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.player.receivable.closed. Partition key: receivableId.
| field | type | required |
|---|---|---|
changeType | string ["CLOSED"] | yes |
receivableId | string | yes |
correctionLineId | string | no |
playerLinkId | string | no |
amount | object | no |
closedReason | string | no |
player.receivable.opened
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.player.receivable.opened. Partition key: receivableId.
| field | type | required |
|---|---|---|
changeType | string ["OPENED"] | yes |
receivableId | string | yes |
correctionLineId | string | no |
playerLinkId | string | no |
amount | object | no |
closedReason | string | no |
position.opened
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.position.opened. Partition key: positionId.
| field | type | required |
|---|---|---|
changeType | string ["OPENED"] | yes |
positionId | string | yes |
orderId | string | yes |
playerLinkId | string | yes |
marketId | string | yes |
outcomeId | string | yes |
state | string | yes |
shares | string | yes |
averageEntryPrice | string | yes |
costBasis | object | yes |
fees | object | yes |
maxSettlementProceeds | object | yes |
position.settled
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.position.settled. Partition key: positionId.
| field | type | required |
|---|---|---|
changeType | string ["SETTLED"] | yes |
positionId | string | yes |
playerLinkId | string | yes |
marketId | string | yes |
outcomeId | string | yes |
state | string ["SETTLED","VOIDED"] | yes |
settlement | object | yes |
position.voided
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.position.voided. Partition key: positionId.
| field | type | required |
|---|---|---|
changeType | string ["VOIDED"] | yes |
positionId | string | yes |
playerLinkId | string | yes |
marketId | string | yes |
outcomeId | string | yes |
state | string ["SETTLED","VOIDED"] | yes |
settlement | object | yes |
sell.completed
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.sell.completed. Partition key: sellId.
| field | type | required |
|---|---|---|
changeType | string ["COMPLETED"] | yes |
sellId | string | yes |
quoteId | string | yes |
positionId | string | yes |
playerLinkId | string | yes |
kind | string ["FULL","PARTIAL"] | yes |
state | string | yes |
shares | string | yes |
filledShares | ? | yes |
sell.failed
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.sell.failed. Partition key: sellId.
| field | type | required |
|---|---|---|
changeType | string ["FAILED"] | yes |
sellId | string | yes |
quoteId | string | yes |
positionId | string | yes |
playerLinkId | string | yes |
kind | string ["FULL","PARTIAL"] | yes |
state | string | yes |
shares | string | yes |
filledShares | ? | yes |
settlement.line_settled
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.settlement.line_settled. Partition key: settlementLineId.
| field | type | required |
|---|---|---|
changeType | string ["SETTLED"] | yes |
settlementLineId | string | yes |
settlementRunId | string | yes |
positionId | string | yes |
outcome | string | yes |
settlementProceeds | object | yes |
feeRefund | object | yes |
wallet.operation.escalated
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.wallet.operation.escalated. Partition key: operationId.
| field | type | required |
|---|---|---|
changeType | string ["ESCALATED"] | yes |
operationId | string | yes |
type | string | yes |
status | string ["SUCCEEDED","REJECTED","ESCALATED"] | yes |
amount | object | yes |
rejectionCode | string,null | yes |
business | object | yes |
wallet.operation.finalized
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.wallet.operation.finalized. Partition key: operationId.
| field | type | required |
|---|---|---|
changeType | string ["SUCCEEDED","REJECTED"] | yes |
operationId | string | yes |
type | string | yes |
status | string ["SUCCEEDED","REJECTED","ESCALATED"] | yes |
amount | object | yes |
rejectionCode | string,null | yes |
business | object | yes |
write_off.approved
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.write_off.approved. Partition key: writeOffId.
| field | type | required |
|---|---|---|
changeType | string ["APPROVED"] | yes |
writeOffId | string | yes |
caseId | string | no |
receivableId | ? | yes |
brandId | ? | no |
amount | object | no |
amountUsd | string | yes |
reasonCode | string | no |
seniorRequired | boolean | no |
band | string | no |
expiresAt | string | no |
signers | integer | no |
receivableOutstanding | ? | no |
approvedAt | string | no |
write_off.requested
x-status: not set in the document (no claim made). CloudEvents type neun.predictions.write_off.requested. Partition key: writeOffId.
| field | type | required |
|---|---|---|
changeType | string ["REQUESTED"] | yes |
writeOffId | string | yes |
caseId | string | no |
receivableId | ? | yes |
brandId | ? | no |
amount | object | no |
amountUsd | string | yes |
reasonCode | string | no |
seniorRequired | boolean | no |
band | string | no |
expiresAt | string | no |
signers | integer | no |
receivableOutstanding | ? | no |
approvedAt | string | no |