Skip to main content

Matchmaking HTTP API

The HTTP API advertises devices, finds compatible peers, and reserves a pair while they establish a direct connection. It exchanges addresses; it does not carry application traffic, perform STUN for clients, or provide a relay.

Ordinary SDK applications use connection matchmaking through the socket API and do not call this endpoint themselves. This reference is for compatible clients, controller implementations, and service development.

Spectranext does all of these calls automatically. For the complete discovery and transport sequence, see STUN and connection setup.

Endpoint and request envelope​

POST https://match.spectranext.net/match
Content-Type: application/json
X-Match-Instance: menu-a-1
X-Match-Request: 1
HeaderMeaning
Content-Typeapplication/json
X-Match-InstanceA fresh identifier for this device's menu invocation. Keep it unchanged until leaving. Allowed: 1–64 letters, digits, _, or -. Generate a new random identifier for each invocation; menu-a-1 below is illustrative.
X-Match-RequestA positive decimal request number, increasing for each new request within the invocation. At most 16 digits; start at 1.

Bodies are JSON objects of at most 4096 bytes. Successful responses use HTTP 200, including an empty JSON object for acknowledgements. API responses use Cache-Control: no-store.

There is no state field. Response shape tells the client whether it received a peer list, a pairing, or an acknowledgement:

ResponseMeaning
{"peers": [...]}Waiting; show compatible available devices.
{"connection": "...", "peer": {...}}Paired; start the direct connection.
{}The requested completion, cancellation, or leave action was acknowledged.
{"error": "..."} with non-200 statusThe request failed.

A waiting response omits connection; it does not return "connection": null.

Fields and address ownership​

Every request includes service and device.

FieldRules
service1–32 lowercase letters, digits, ., _, or -; the part before @match.spx.
deviceStable device identifier. Spectranext uses the canonical 16 hexadecimal serial digits, without display separators. The service accepts 1–64 letters, digits, _, or - for compatible clients.
addressObject containing exactly ip and port. ip is an IPv4 string; port is an integer from 1 to 65535. Required except for cancel and leave, where it must be omitted.
nameWaiting advertisements only. Optional, at most 23 printable ASCII characters. An omitted, null, empty, or space-only name falls back to the serial.
codeWaiting advertisements only. Optional string from "1000" through "9999". Omit to disable the requirement.
transportWaiting advertisements only. "tcp" or "udp"; omission means "tcp".

An advertisement replaces the waiting profile. Send the current name, code requirement, and transport on each refresh; omitting code turns it off, and omitting transport selects TCP.

The client performs STUN before each new address-bearing POST and supplies the observed UDP endpoint in address. Use the same open UDP socket for discovery and the eventual direct transport, keeping it open throughout the match. An HTTP client's source address or a STUN result from a different UDP socket is not a substitute.

For a TCP application, this is the outer UDP transport address, not an application TCP listening port. In development, an explicitly configured LAN STUN fixture may return a LAN address. The public service does not discover or rewrite these endpoints.

Once paired, the advertised addresses are fixed for that connection. A different address produces 409 address_changed and cancels the pending connection; start a new matchmaking attempt instead of silently changing its endpoint.

Request scenarios​

The examples use:

ParticipantDeviceInstanceUDP endpoint
A1111111111111111menu-a-1198.51.100.10:41000
B2222222222222222menu-b-1203.0.113.20:52000

The addresses are documentation examples. Each JSON request below is a POST /match with the envelope headers above and the stated participant's instance and request number.

1. Advertise and refresh​

A, request 1:

{
"service": "chess.v1",
"device": "1111111111111111",
"name": "Alex",
"address": {"ip": "198.51.100.10", "port": 41000}
}

HTTP 200, when no compatible peer is available:

{"peers": []}

B, request 1: advertise with a joining code:

{
"service": "chess.v1",
"device": "2222222222222222",
"name": "Mira",
"code": "1337",
"address": {"ip": "203.0.113.20", "port": 52000}
}

HTTP 200:

{
"peers": [
{"device": "1111111111111111", "name": "Alex", "requires_code": false}
]
}

A, request 2: repeat A's advertisement after fresh STUN. Its response now includes B:

{
"peers": [
{"device": "2222222222222222", "name": "Mira", "requires_code": true}
]
}

Only peers with the same service and transport are listed. The requesting device, expired devices, and reserved peers are excluded. device is the selection identifier; it is not an IP address or a row number. Names need not be unique. Peer addresses are returned only after pairing.

The HTTP response has no nine-peer cap. The device menu displays at most nine peers and maintains stable local selection slots.

To advertise a UDP application, include "transport": "udp" on every advertisement. Do not repeat it in selection or completion requests; the reservation inherits the advertised transport.

2. Select a peer​

A, request 3:

{
"service": "chess.v1",
"device": "1111111111111111",
"connect_to": "2222222222222222",
"join_code": "1337",
"address": {"ip": "198.51.100.10", "port": 41000}
}

Omit join_code when the target does not require one. It is a string of exactly four digits when supplied. Do not include name, code, or transport in this request.

HTTP 200:

{
"connection": "12345678-1234-1234-1234-123456789abc",
"peer": {
"device": "2222222222222222",
"name": "Mira",
"address": {"ip": "203.0.113.20", "port": 52000}
}
}

This atomically reserves both devices. A can immediately start direct transport setup using B's endpoint. The connection value identifies this reservation; retain it unchanged.

A wrong or missing required code returns HTTP 409:

{"error": "wrong_code"}

That attempt reserves neither device. Let the user correct the code and issue a new request number. A target that has left, is incompatible, or is already reserved produces the corresponding conflict response below.

Only the selected target's code is checked. A device requiring a code may still initiate a connection to an open peer. Concurrent A→B and B→A selections converge on the same pair rather than creating two connections.

3. Receive an incoming selection​

B, request 2: send its next ordinary advertisement, including its current name, code, and STUN address as in step 1.

Because A has selected B, the response is a pairing instead of a peer list:

{
"connection": "12345678-1234-1234-1234-123456789abc",
"peer": {
"device": "1111111111111111",
"name": "Alex",
"address": {"ip": "198.51.100.10", "port": 41000}
}
}

B starts direct transport setup using A's endpoint. No second user confirmation or HTTP acceptance action is required.

4. Query a reserved connection, if needed​

A compatible client can query the reservation using A, request 4:

{
"service": "chess.v1",
"device": "1111111111111111",
"connection": "12345678-1234-1234-1234-123456789abc",
"address": {"ip": "198.51.100.10", "port": 41000}
}

HTTP 200: the same pairing shape as step 2. There is no poll field: a request containing connection with no other action is a query.

Current Spectranext/FuseX clients do not issue these HTTP polls while setting up an already known pair. They check direct transport readiness locally so a slow HTTP response cannot delay a completed connection. Cancellation or expiry can still be reported to clients that query the reservation.

5. Report completion​

Once its direct transport is usable, A, request 5 reports success:

{
"service": "chess.v1",
"device": "1111111111111111",
"connection": "12345678-1234-1234-1234-123456789abc",
"connected": true,
"address": {"ip": "198.51.100.10", "port": 41000}
}

HTTP 200:

{}

B sends its own connected: true request with its instance, next request number, device, and address. Once both reports arrive, the server marks the pair complete and expires their waiting presence.

The HTTP acknowledgement is housekeeping. Publish connection success to the application before waiting for it. Current clients make one best-effort completion attempt afterward. A failed or lost report does not close an established application connection; server record expiry is not a transport-close command.

Request numbers need only increase: if A skipped optional step 4, it may use request 4 for completion instead.

6. Cancel an unfinished connection​

As an alternative to completion, send:

{
"service": "chess.v1",
"device": "1111111111111111",
"connection": "12345678-1234-1234-1234-123456789abc",
"cancel": true
}

Use A's next request number. HTTP 200:

{}

Do not include address. The other participant's next reservation query or advertisement returns HTTP 409:

{"error": "peer_cancelled"}

A client already performing direct setup without HTTP polling detects failure through the transport or its setup timeout. Close the pending transport and advertise again, or leave. Cancellation alone does not re-advertise the cancelling device.

7. Leave the menu​

{
"service": "chess.v1",
"device": "1111111111111111",
"leave": true
}

Use A's next request number. HTTP 200:

{}

Omit address and connection. Leaving removes presence, cancels unfinished setup, and retires this instance. A later menu invocation must use a new instance identifier. Local cancellation and cleanup must still work when the HTTP service is unavailable.

Retries and concurrent requests​

For a lost response, retry the most recent request with the same body, instance, and request number. Reuse its already discovered address; do not rerun STUN and change the body of a retry. The server returns the stored result, including stored error responses.

  • Same number with a different body: 400 invalid_request.
  • An older number, or a request from a retired instance: 409 instance_expired.
  • Correcting input or refreshing STUN for a new operation: use a new number.
  • Opening a new instance for the same service/device retires the old instance and cancels its unfinished reservation.

Serialize requests for one invocation. The service retains the latest request result rather than a cache of every request number. Use bounded retries; current clients retry a lost ordinary response once, and do not retry completion reporting on the connection's critical path.

Expiry and errors​

TimerValue
Waiting presence lease40 seconds after the last successful waiting advertisement
Pairing deadline60 seconds from reservation
Retained completed/cancelled/expired connection records120 seconds

Current waiting menus refresh about one second after the previous refresh completes. A reserved peer is not offered to someone else merely because its waiting lease expires. Connection queries do not extend the pairing deadline.

HTTP statuserrorMeaning / response
400invalid_requestInvalid headers, fields, types, size, or request-number reuse with a changed body. Fix the request.
409wrong_codeAsk for a corrected joining code.
409peer_goneTarget disappeared, is self, or has an incompatible transport. Refresh the list.
409peer_busyTarget is reserved by another connection. Refresh the list.
409peer_cancelledThe other side cancelled unfinished setup. Close it and retry or leave.
409address_changedThe paired endpoint changed. Start a new attempt.
409connection_expiredThe reservation is unknown, expired, or not owned by this invocation.
409instance_expiredPresence/invocation is missing, stale, superseded, or retired. Register afresh; use a new instance for a new menu invocation.
503service_unavailableBackend failure. Retry within the client's bounded retry policy.
404not_foundUnknown endpoint or unsupported method.

Gateway throttling and network failures may return other status codes, non-JSON responses, or no response. Do not treat those as a successful empty acknowledgement. Joining codes, names, and device identifiers are convenience/routing fields, not authenticated identities; the API has no account login or client API-key requirement.

Local service and runtime configuration​

From the repository root, run the self-contained Python reference service:

make -C machmaking run
make -C machmaking test

The directory is spelled machmaking in the repository. The local URL is http://127.0.0.1:8080/match. Stop it with make -C machmaking stop.

For native FuseX, set these in its launch environment and restart it:

SPX_MATCH_API=http://127.0.0.1:8080/match
SPX_MATCH_LOCAL_HTTP=1

For hardware or emulators on another machine, publish the API on the host's LAN address:

make -C machmaking run BIND=192.168.1.10

Replace the example address with the service host's actual address. 127.0.0.1 on the other machine does not refer to that host. If a container is already running, stop it before changing its bind address.

FuseX environment variableDefault
SPX_MATCH_APIhttps://match.spectranext.net/match
SPX_MATCH_STUN_HOSTstun.l.google.com
SPX_MATCH_STUN_PORT19302
SPX_MATCH_LOCAL_HTTP0; only 1 permits development HTTP

On RP2350, use the existing controller CLI to inspect or override endpoints for the current run:

spx exec --follow 2 'match-config'
spx exec --follow 2 'match-config https://your-api.example/match stun.l.google.com 19302'

For an explicitly permitted development HTTP service, append 1:

spx exec --follow 2 'match-config http://192.168.1.10:8080/match stun.l.google.com 19302 1'

Close the current match before changing settings. Hardware overrides revert to defaults after a controller reboot. FuseX's HTTPS certificate verification is currently disabled; integration with system or Mozilla trust roots is deferred. RP2350 retains its existing Wi-Fi TLS verification.

A local API is not a local STUN server and does not change UDP reachability. Two devices sharing a router may need LAN endpoint discovery when that router cannot hairpin public UDP traffic. Conversely, a VPN does not by itself guarantee usable UDP mappings. Test the advertised path rather than assuming it works.

For a service health check:

GET https://match.spectranext.net/health

HTTP 200: {"ok": true}. This checks the handler, not peer traversal or the complete matchmaking lifecycle. The Docker service's make test runs its API tests and an HTTP startup smoke check.

Match initiator metadata​

Every paired response also includes initiator, the device ID that created the reservation through connect_to. The value is identical for both members and stays fixed through polling, opposite selections, retries, and completion. The first committed reservation wins simultaneous selections; it does not select which device initiates inner TCP. Example:

{
"connection": "12345678-1234-1234-1234-123456789abc",
"initiator": "a000000000000101",
"peer": {
"device": "a000000000000102",
"name": "Mira",
"address": {"ip": "192.0.2.2", "port": 40002}
}
}

This is an additive response field. Clients accept older services without it; the initiator string is then unavailable. Sessions created before a service upgrade may return null until a new match is created.