Skip to content

Outcome Delivery

An outcome is anything IVK Match reports to the game backend 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.

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. If the outcome is lost, those players are stranded on a screen that never advances.

Delivery therefore has to survive a backend that was down for thirty seconds during a deploy and pick up exactly where it left off, without requiring the integration 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 the consumer acknowledges it. A failed webhook or a dropped WebSocket delays delivery; it does not lose it. Durability is handled by IVK Match; the integration only chooses how outcomes arrive.

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 delivery can be repointed with a configuration edit rather than a deploy. See Queue Configuration § config.outcomes.


SinkDeliveryIntegration providesBest for
WebhookPush, at-least-once, retriedAn HTTPS endpoint and a shared secretMost integrations. Serverless backends. Quickest to integrate
WebSocketPush, at-least-once, resumableA consumer that reconnects and tracks an offsetBackends that cannot expose an inbound endpoint

This sink requires the least integration work. IVK Match pushes outcomes to a configured HTTPS URL. There is no broker to run, no client library, and no polling loop, only an HTTP handler.

POST https://backend.example.com/ivk/outcomes
Content-Type: application/x-protobuf (or application/json)
X-IVK-Delivery: 0f8c… unique per delivery attempt
X-IVK-Outcome-Id: 3f2b… stable per outcome; dedupe on this
X-IVK-Queue-Id: acme.quickplay
X-IVK-Kind: match.created
X-IVK-Timestamp: 1753600000
X-IVK-Signature: sha256=… HMAC over timestamp + body

Intended semantics:

AspectBehaviour
SuccessAny 2xx. The outcome is acknowledged and will not be sent again.
FailureNon-2xx, timeout, or connection error; retried with exponential backoff and jitter
Retry budgetBounded; on exhaustion the delivery is parked and surfaced as an alertable metric rather than dropped
OrderingPer queue, in order. A stuck delivery blocks that queue’s subsequent outcomes rather than reordering around it
DuplicatesPossible. Dedupe on X-IVK-Outcome-Id.
BatchingOptional; several outcomes per request to cut round trips at high volume
EncodingProtobuf by default; JSON available for backends that cannot decode protobuf
AuthHMAC-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: 8

The receiving handler should ack fast and process asynchronously: persist the outcome, return 200, and do the provisioning work on the backend’s 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.

Per-queue in-order delivery means a handler that consistently fails on one poisonous outcome halts that queue’s deliveries. This is preferred over skipping a match, but it makes the parked-delivery metric something to alert on.


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=1234
Upgrade: websocket

Intended semantics:

  • Subscribe to one queue, a namespace prefix (acme.>), or everything.
  • Resumable: connect with from_offset to 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: websocket

Webhook is the recommended default unless the backend cannot accept an inbound connection. It matches how backends typically integrate 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: the consumer is behind NAT, it is a dedicated-server process, it is a long-lived worker, or the network policy is egress-only.

The choice is where the bookkeeping lives. Webhooks keep retries on the IVK Match side and require an endpoint. WebSockets need no endpoint and cut latency, but the 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.


Identical on both sinks:

GuaranteeHolds?Notes
At-least-once deliveryYesDuplicates are possible on either sink
Exactly-once deliveryNoDedupe on Outcome.id or Match.id
Per-queue orderingYesOutcomes for one queue arrive in occurrence order
Global ordering across queuesNoIndependent queues are independent
Survives the consumer being downYesBounded by the retention window
Survives an IVK Match service restartYesOutcomes 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.

The consumer should 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 more work and gives a worse result.