Skip to content

Integration Flow

End-to-end sequences from a player pressing “Play” to that player connecting to a server.


ParticipantRole
PlayerGame client. Presses Play, waits, receives connect info.
Game BackendThe customer’s service. Owns player identity, skill ratings, session state. The only thing that talks to IVK Match.
IVK MatchThe matchmaker — the managed service, or a self-hosted container at a later stage. Holds the ticket pool, forms matches, emits outcomes.
OrchestratorThe platform’s gameserver provisioning API.
Game ServerThe provisioned dedicated server process.

One rule underpins everything: IVK Match never talks to the player, and the player never talks to IVK Match. The game backend is always in the middle, which keeps authentication, anti-cheat, skill lookup, and rate limiting where they belong.

The diagrams below name operations by their gRPC name (CreateTicket, CancelTicket, and so on). Every one of them is equally available as a plain HTTP/JSON call, and the flows are identical either way. See Matchmaking API reference.


The recommended integration. Works with the current build.

sequenceDiagram
    autonumber
    actor P as Player
    participant GB as Game Backend
    participant IM as IVK Match
    participant OR as Orchestrator
    participant GS as Game Server

    P->>GB: queue_matchmaking(playlist, party)
    Note over GB: Look up MMR, measure/read latencies,<br/>resolve platform + crossplay prefs
    GB->>GB: ticket_id = uuid4()
    GB->>IM: CreateTicket(ticket_id, queue_id, ruleset_ids,<br/>engine_input, metadata)
    IM-->>GB: ticket_id (ack, ticket is in the pool)
    GB-->>P: queued (ticket_id)

    Note over IM: Every tick (default 1s) the engine improves<br/>the matches it is assembling from the pool

    IM->>IM: match formed, tickets leave the pool
    IM->>GB: outcome: match.created<br/>{ match_id, tickets[metadata], engine_output }
    Note over GB: Decode engine_output →<br/>teams + host.preferred

    GB->>OR: CreateGameServer(host=preferred, match_id, players)
    OR->>GS: start
    GS-->>OR: ready (address, port)
    OR-->>GB: server { address, port }

    GB->>GB: persist session(match_id → server, players)
    GB-->>P: game_found { match_id, address, port, team }
    P->>GS: connect

Step 2: enrich before submitting. IVK Match knows nothing about the player. Everything the engine needs goes in engine_input: player IDs, MMR, per-host latencies, and the attributes used by the queue’s attr_filter. Everything the backend wants back goes in metadata.

Step 4: the backend owns the ticket ID. Generate a UUID before calling. If the call times out, retry with the same ID; it cannot create a duplicate.

Step 5: the ack is not a match. CreateTicket returns when the ticket is durably pooled. Show the player a queue state, not a match.

Step 8: the outcome is the match. By the time match.created is delivered, the tickets have already left the pool. That is a commitment: those players are no longer matchmaking. A backend that cannot honour it has to compensate actively; see below.

Step 9: host.preferred drives placement. The latency map keys the backend supplied in engine_input come back as engine_output.host.preferred, with every mutually-acceptable alternative in host.acceptable. If those keys are the platform’s own region or pool identifiers, this step is a direct pass-through with no translation layer.

Step 10: provisioning is the backend’s call. The orchestrator API, its authentication, and its retry policy all stay on the platform’s side of the boundary.

Step 13: correlate via match_id. Match.id is a UUID and the natural session key. It is also the value the game server will later need for backfill requests.

PhaseTypicalDriven by
CreateTicket round tripsingle-digit msNetwork
Waiting in queueseconds to minutesPopulation, wait_time, ruleset strictness
Match formed → outcome deliveredtens of msSink; webhook adds one HTTP round trip
Server provisioningsecondsOrchestrator; usually the dominant term after queue wait
Notify playermsThe backend’s own push channel

Queue wait and server provisioning dominate. tick_rate_secs: 1 means the engine contributes at most one second of scheduling latency.


The player backs out. There is a race here, and you have to handle it.

sequenceDiagram
    autonumber
    actor P as Player
    participant GB as Game Backend
    participant IM as IVK Match

    P->>GB: cancel_matchmaking(ticket_id)
    GB->>IM: CancelTicket(queue_id, ticket_id)

    alt Ticket still waiting in the pool
        IM-->>GB: ok
        GB-->>P: cancelled
    else Ticket already committed to a match
        IM-->>GB: FAILED_PRECONDITION
        Note over GB: The match is already formed.<br/>Its outcome is in flight.
        GB-->>P: too late, match found
        IM->>GB: outcome: match.created
        GB->>GB: proceed with provisioning
    end

A failed CancelTicket does not mean the player is still in queue. It usually means the opposite, so treat FAILED_PRECONDITION as “a match is coming, prepare to honour it”. Drop the match.created that follows and you strand every other player in that match.


The match is real but no server can be had. Those players go back into the pool; do not dump them out to the menu.

sequenceDiagram
    autonumber
    participant IM as IVK Match
    participant GB as Game Backend
    participant OR as Orchestrator
    actor P as Players

    IM->>GB: outcome: match.created { match_id, tickets, host }
    GB->>OR: CreateGameServer(host = host.preferred)
    OR-->>GB: error, capacity exhausted

    loop for each host in host.acceptable
        GB->>OR: CreateGameServer(host)
        OR-->>GB: error
    end

    Note over GB: No server available on any acceptable host
    GB->>IM: ReactivateTickets(queue_id, ticket_ids)
    IM-->>GB: failed_ticket_ids: []
    Note over IM: Tickets are back in the pool,<br/>keeping their original created_at
    GB-->>P: still searching (no error shown)

Three things make this work:

host.acceptable gives you a fallback list for free. Every host in it is tolerable to every ticket in the match. Walk it before giving up.

ReactivateTickets restores queue position. Tickets keep their original created_at, so a player who waited 90 seconds does not go to the back of the line.

terminal_retention_ttl_secs is your compensation window. A resolved ticket can only be reactivated while it is still retained. Any ticket in failed_ticket_ids was evicted and needs a fresh CreateTicket. Set pool.tickets.terminal_retention_ttl_secs above the orchestrator’s worst-case provisioning timeout plus the fallback walk. The default of 300 s is comfortable for most platforms; production configs use 3600 s where provisioning can be slow.


A ticket that never matches expires on its own.

sequenceDiagram
    autonumber
    actor P as Player
    participant GB as Game Backend
    participant IM as IVK Match

    GB->>IM: CreateTicket(...)
    Note over IM: No compatible opponents found within<br/>pool.tickets.expiration_ttl_secs
    IM->>IM: cleanup pass evicts the ticket
    IM->>GB: outcome: ticket.expired { ticket_ids: [...] }
    GB-->>P: search timed out, retry / widen filters?

expiration_ttl_secs is effectively the maximum time a player can sit in queue. Set it a little above the longest wait the game is willing to display, so the timeout the player sees is the game’s decision and not a surprise from the matchmaker.

ticket.expired is batched: one outcome can carry many ticket IDs.


A running match has open slots: someone disconnected, or it started under-full.

sequenceDiagram
    autonumber
    participant GS as Game Server
    participant GB as Game Backend
    participant IM as IVK Match
    actor NP as New Player

    Note over GS: A player leaves, 2 slots open
    GS->>GB: report_open_slots(match_id, 2)
    GB->>IM: CreateBackfillRequest(queue_id, match_id,<br/>ruleset_id, slots_requested=2, engine_input)
    IM-->>GB: backfill_id

    NP->>GB: queue_matchmaking(...)
    GB->>IM: CreateTicket(...)

    IM->>IM: backfill stage assigns tickets to the request
    IM->>GB: outcome: backfill.created<br/>{ request, assigned_tickets }
    GB-->>NP: game_found { address, port }
    NP->>GS: connect

    Note over GS: Match is full again
    GS->>GB: report_full(match_id)
    GB->>IM: CancelBackfillRequests(queue_id, [backfill_id])

Worth knowing:

  • A new request for the same match_id replaces the previous one, so a running server can re-post its current open-slot count on a timer instead of tracking request state.
  • ruleset_id is required whenever the queue has more than one ruleset.
  • Backfill.request echoes the original request in full, so the outcome is self-describing: no side lookup to work out which match the new players belong to.
  • Cancel the request when the match fills or ends. Otherwise it sits until backfill_requests.expiration_ttl_secs and emits backfill.expired.

Planned. The backfill API, pool model, and outcome types are all in place, but the engine does not yet fulfil backfill requests: its backfill stage is currently a no-op. A request submitted today is accepted and pooled, and eventually emits backfill.expired instead of backfill.created. Work on it is underway. Design against the flow above, but do not gate a launch on it until it ships.


Every state the backend can observe, and what moves between them.

stateDiagram-v2
    [*] --> Active: CreateTicket
    Active --> Resolved: match formed → match.created
    Active --> Cancelled: CancelTicket
    Active --> Expired: expiration_ttl_secs elapsed → ticket.expired
    Active --> Removed: queue cleared / retired / params changed → ticket.removed
    Resolved --> Active: ReactivateTickets
    Resolved --> [*]: terminal_retention_ttl_secs elapsed
    Cancelled --> [*]: terminal_retention_ttl_secs elapsed
    Expired --> [*]: terminal_retention_ttl_secs elapsed
    Removed --> [*]

The edge to remember is Resolved → Active via ReactivateTickets. It only exists while the ticket is still retained, and it is the only way to undo a match the backend cannot honour.


Planned. Requires a provisioning integration inside IVK Match.

IVK Match calls the orchestrator itself and returns a fully-resolved match, server address included.

sequenceDiagram
    autonumber
    actor P as Player
    participant GB as Game Backend
    participant IM as IVK Match
    participant OR as Orchestrator
    participant GS as Game Server

    P->>GB: queue_matchmaking(...)
    GB->>IM: CreateTicket(...)
    IM-->>GB: ticket_id
    GB-->>P: queued

    IM->>IM: match formed
    IM->>OR: CreateGameServer(host = preferred, match_id)
    OR->>GS: start
    GS-->>OR: ready
    OR-->>IM: server { address, port }

    IM->>GB: outcome: match.created<br/>{ match_id, tickets, teams, server{address,port} }
    GB-->>P: game_found { address, port, team }
    P->>GS: connect
  • A customer with no backend at all can integrate: submit tickets, receive a match that is ready to join.
  • One fewer round trip and one fewer failure surface in customer code.
  • Provisioning failures are retried and compensated inside IVK Match, which already owns the pool. Reactivating tickets becomes an internal operation instead of a contract the customer has to implement correctly.
  • The platform expresses placement policy (capacity, cost, region affinity) once, in the matchmaker’s provisioning config, instead of relying on every customer’s backend to get it right.
  • A provisioning driver in IVK Match: an outbound HTTP client for the orchestrator’s API, or a generic webhook contract the platform implements on its side.
  • Credential management per tenant, and a place to configure the mapping from host keys to orchestrator placement parameters.
  • A rollback path: when provisioning fails on every acceptable host, the match is abandoned and its tickets returned to the pool, with the attempt bounded by a timeout so a slow orchestrator cannot stall the queue.
  • An extension to the Match message carrying the server address, and a decision about whether that is engine-agnostic (on Match) or engine-specific (inside engine_output). Engine-agnostic is the better shape.
  • Idempotency against the orchestrator, so a retried provisioning call does not leak servers.

Rather than IVK Match speaking any particular orchestrator’s API, it can POST a provisioning request to a platform-supplied URL and await a response:

POST <PROVISION_WEBHOOK_URL>
{
"match_id": "…",
"queue_id": "acme.quickplay",
"ruleset_id": "5v5",
"preferred_host": "eu-central",
"acceptable_hosts": ["eu-central", "eu-west"],
"player_count": 10,
"teams": [ { "id": "a", "player_ids": ["…"] }, … ]
}
→ 200 { "address": "1.2.3.4", "port": 27015, "host": "eu-central" }
→ 503 { "error": "no_capacity" } // IVK Match tries the next acceptable host

This keeps the orchestrator’s API private to the platform, and IVK Match ships one integration instead of one per orchestrator. It is the recommended shape for Model B.


For a game team wiring up Model A:

  • Generate ticket IDs as UUIDs in the backend; retry CreateTicket with the same ID on timeout.
  • Put everything the engine needs in engine_input; everything you want back in metadata.
  • Use host keys in Player.latencies that map directly onto the platform’s placement identifiers.
  • Consume outcomes idempotently, keyed on Outcome.id or Match.id.
  • Handle match.created as a commitment: provision, or compensate with ReactivateTickets.
  • Walk host.acceptable before declaring provisioning failed.
  • Set terminal_retention_ttl_secs above the worst-case provisioning time.
  • Handle FAILED_PRECONDITION on CancelTicket as “a match is coming”.
  • Retry UNAVAILABLE with backoff rather than surfacing it to the player.
  • Handle ticket.expired and ticket.removed; both mean the player is out of the pool and needs to be told or re-queued.
  • Cancel backfill requests when a match fills or ends.