Outcome Delivery
An outcome is anything IVK Match has to tell you about asynchronously: a match was formed, a backfill was filled, tickets expired or were removed. The message schema is identical across every sink; see Matchmaking API reference § Outcome schema. What follows is how those outcomes reach the game backend.
Why the delivery path matters
Section titled “Why the delivery path matters”A formed match is a commitment. By the time an outcome exists, the tickets have already left the pool and those players are no longer matchmaking. Lose the outcome and they are stranded, waiting on a screen that will never advance.
So delivery is not an implementation detail. It has to survive a backend that was down for thirty seconds during a deploy and pick up exactly where it left off, without asking you to operate a message broker to receive a match.
Under both sinks an outcome is recorded durably before it is delivered, and is not considered handled until you acknowledge it. A failed webhook or a dropped WebSocket delays delivery; it does not lose it. That part is ours to get right. Your choice is only how outcomes arrive.
Chosen per queue
Section titled “Chosen per queue”The sink is part of the queue’s configuration, not a game-wide setting. Two queues on the same
game can deliver to different endpoints, and you can repoint delivery with a configuration edit
rather than a deploy. See
Queue Configuration § config.outcomes.
Sink comparison
Section titled “Sink comparison”| Sink | Delivery | You provide | Best for |
|---|---|---|---|
| Webhook | Push, at-least-once, retried | An HTTPS endpoint and a shared secret | Most integrations. Serverless backends. The fastest path to working |
| WebSocket | Push, at-least-once, resumable | A consumer that reconnects and tracks an offset | Backends that cannot expose an inbound endpoint |
Webhooks
Section titled “Webhooks”This is the least work to integrate. Give IVK Match an HTTPS URL and it pushes outcomes to it. There is no broker to run, no client library, and no polling loop, just an HTTP handler.
POST https://backend.example.com/ivk/outcomesContent-Type: application/x-protobuf (or application/json)X-IVK-Delivery: 0f8c… unique per delivery attemptX-IVK-Outcome-Id: 3f2b… stable per outcome; dedupe on thisX-IVK-Queue-Id: acme.quickplayX-IVK-Kind: match.createdX-IVK-Timestamp: 1753600000X-IVK-Signature: sha256=… HMAC over timestamp + bodyIntended semantics:
| Aspect | Behaviour |
|---|---|
| Success | Any 2xx. The outcome is acknowledged and will not be sent again. |
| Failure | Non-2xx, timeout, or connection error; retried with exponential backoff and jitter |
| Retry budget | Bounded; on exhaustion the delivery is parked and surfaced as an alertable metric rather than dropped |
| Ordering | Per queue, in order. A stuck delivery blocks that queue’s subsequent outcomes rather than reordering around it |
| Duplicates | Possible. Dedupe on X-IVK-Outcome-Id. |
| Batching | Optional; several outcomes per request to cut round trips at high volume |
| Encoding | Protobuf by default; JSON available for backends that cannot decode protobuf |
| Auth | HMAC-SHA256 signature over timestamp and body using the configured secret. Verify it, and reject stale timestamps to prevent replay. |
Configuration, in the queue file:
config: outcomes: sink: webhook webhook: url: https://backend.example.com/ivk/outcomes secret: ${ACME_OUTCOME_SECRET} encoding: protobuf # or json timeout_ms: 5000 max_retries: 8For the receiving handler: ack fast, process asynchronously. Persist the outcome,
return 200, and do the provisioning work on your own queue. A handler that provisions a
server before responding will hit the delivery timeout and cause a retry storm, with
duplicate provisioning attempts as the visible symptom.
The ordering guarantee cuts both ways. Per-queue in-order delivery means a handler that consistently fails on one poisonous outcome halts that queue’s deliveries. Skipping a match would be worse, so this is the right default, but it does make the parked-delivery metric something to alert on.
WebSocket
Section titled “WebSocket”A persistent stream for backends that want push delivery without exposing a public HTTPS endpoint: useful when the backend is behind NAT, is a dedicated-server process, or is a long-lived worker.
GET /v1/outcomes/stream?queue_id=acme.quickplay&from_offset=1234Upgrade: websocketIntended semantics:
- Subscribe to one queue, a namespace prefix (
acme.>), or everything. - Resumable: connect with
from_offsetto replay everything missed while disconnected, or omit it to start from the newest outcome. - The client acks offsets on the same socket; acks advance the durable consumer position.
- Heartbeats and server-side idle timeouts detect half-open connections.
- Multiple concurrent consumers with independent offsets.
Compared to webhooks: lower latency, no inbound HTTPS endpoint required, but the client owns reconnection and offset bookkeeping.
Configuration, in the queue file:
config: outcomes: sink: websocketChoosing a sink
Section titled “Choosing a sink”Start with webhook unless you cannot accept an inbound connection. It matches how your backend already integrates with payment providers and platform services, it needs no client library, and there is no connection state to manage.
Choose WebSocket when an inbound HTTPS endpoint is not available to you: the consumer is behind NAT, it is a dedicated-server process, it is a long-lived worker, or your network policy is egress-only.
The choice is where the bookkeeping lives. Webhooks keep retries on our side and ask you for an endpoint. WebSockets need no endpoint and cut latency, but your client owns reconnection and offset tracking.
Either way the sink is queue configuration, so moving from one to the other is an edit rather than a migration.
Delivery guarantees
Section titled “Delivery guarantees”Identical on both sinks:
| Guarantee | Holds? | Notes |
|---|---|---|
| At-least-once delivery | Yes | Duplicates are possible on either sink |
| Exactly-once delivery | No | Dedupe on Outcome.id or Match.id |
| Per-queue ordering | Yes | Outcomes for one queue arrive in occurrence order |
| Global ordering across queues | No | Independent queues are independent |
| Survives your consumer being down | Yes | Bounded by the retention window |
| Survives a service restart on our side | Yes | Outcomes are recorded durably before delivery |
Outcome.producer_id and producer_sequence give a monotonic per-producer sequence, which
is useful for detecting gaps: a consumer that sees sequence N+2 after N knows it missed
one and can alert instead of carrying on.
Design the consumer to be idempotent. Either sink can deliver the same outcome twice: after a
retry, a reconnect, or a restart between write and acknowledgement. Key session creation on
Match.id and duplicates stop mattering. Suppressing them at the transport layer is far more work
for a worse result.