Skip to content

.NET SDK

The official C# client for IVK Match. It wraps the gRPC surface, ships the generated message types, and provides helpers for packing engine payloads.

Terminal window
dotnet add package Invokation.Match.Sdk
using Invokation.Match.Sdk;
using var sdk = MatchSdk.CreateBuilder()
.WithBaseUrl("https://match.ivk.dev")
.Build();

The client is disposable and intended to be long-lived. Create one per process and share it; it holds a gRPC channel, and constructing one per request costs you connections and latency.

Builder optionRequiredPurpose
WithBaseUrl(string)YesThe endpoint to connect to
WithRetryConfig(RetryConfig)NoRetry policy for transient failures
WithHttpClient(HttpClient)NoSupply your own HttpClient, for credentials or custom transport behaviour
WithLogger(ILogger)NogRPC diagnostics

Wrap an HttpClient in a handler that adds the Authorization header to every call:

sealed class BearerTokenHandler(string token) : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken ct)
{
request.Headers.Authorization = new("Bearer", token);
return base.SendAsync(request, ct);
}
}
var httpClient = new HttpClient(new BearerTokenHandler(token)
{
InnerHandler = new HttpClientHandler(),
});
using var sdk = MatchSdk.CreateBuilder()
.WithBaseUrl("https://match.ivk.dev")
.WithHttpClient(httpClient)
.Build();

Load the token from your secret store. See Authentication for where credentials belong and how to rotate them.

using Google.Protobuf;
using Matchmaker.Core.V1;
using Matchmaker.Engines.Nemesis.V1;
var input = new EngineInput
{
Players =
{
new Player
{
PlayerId = "alice",
Mmr = 0.62,
Latencies = { ["eu-central"] = 22, ["eu-west"] = 41 },
},
},
Attributes =
{
["playlist"] = new Attribute { Values = { "TDM" } },
["platform"] = new Attribute { Values = { "pc" }, Accepts = { "pc", "xbox" } },
},
};
var ticket = new Ticket
{
Id = Guid.NewGuid().ToString(),
QueueId = "acme.quickplay",
RulesetIds = { "5v5" },
EngineInput = Nemesis.PackInput(input),
Metadata = ByteString.CopyFromUtf8(sessionId),
};
var ticketId = await sdk.CreateTicketAsync(ticket, ct);

Three things this example does deliberately.

You generate the ID, and it must be a UUID. Because the ID is yours, retrying after a timeout is idempotent: the same ID cannot produce two pool entries.

It does not set CreatedAt. The server sets it and overwrites any value you send. It is the basis of queue position, so it has to be server-authoritative.

Metadata comes back verbatim. Nothing reads it, and it rides along on every outcome about this ticket. Put your session and party IDs there instead of keeping a lookup table.

The return value confirms the ticket is durably queued. It is not a match. Matches arrive asynchronously, over the sink configured on the queue.

MethodPurpose
CreateTicketAsync(ticket, ct)Put a player or party into a queue
CancelTicketAsync(queueId, ticketId, ct)Remove a ticket from matchmaking
ReactivateTicketsAsync(queueId, ticketIds, ct)Return resolved tickets to the pool after a match you could not honour
CreateBackfillRequestAsync(backfillRequest, ct)Ask for players to fill a running match
CancelBackfillRequestsAsync(queueId, backfillIds, ct)Withdraw backfill requests
ClearQueuePoolAsync(queueId, ct)Empty a queue’s pool. Test tooling; it ejects every waiting player

Request and response semantics for each are in the Matchmaking API reference.

engine_input and engine_output are engine-defined protobuf messages carried as bytes. Every queue runs Nemesis, and the Nemesis helper packs and unpacks both:

var output = Nemesis.UnpackOutput(match.EngineOutput);
var preferredHost = output.Host.Preferred;
var teams = output.Teams;

The package contains helpers for other engines. They are being deprecated; use Nemesis.

The SDK does not consume outcomes. They are pushed to the sink configured on your queue rather than pulled through the client, so this is a separate path in your backend: an HTTP handler or a WebSocket consumer, not a call you make.

See Outcome Delivery for the sinks, and Integration Flow for what to do when a match arrives.

Failures surface as RpcException carrying the gRPC status codes documented in Matchmaking API § Error semantics. Two deserve specific handling:

  • Unavailable: retry with backoff. Do not surface it to a player. It is usually a brief restart window, and tickets already queued survive it.
  • FailedPrecondition on CancelTicketAsync: the player is very likely already in a match whose outcome is in flight. Never treat a failed cancel as “the player left the queue”.