V-ENT for game studios
Run V-ENT tournaments inside your game
Connect your game once and every V-ENT format runs in it, the ones here today and the ones still to come. Your game only ever handles one game session at a time; V-ENT runs the bracket, the tables, the money and the stream around it.
Connect a gameHow it fits together
- Connect your game in the Studios section of the partner portal. It starts in the sandbox, with a signing secret shown once.
- Players link their game account to their V-ENT account, with a code your game shows or with Sign in with V-ENT.
- An organiser connects their tournament to your game from the tournament console.
- When a match is ready, V-ENT asks your game for a lobby. Your game answers with a session id and how to join.
- When the game ends, your server posts the raw result. V-ENT checks it, scores it with your expression and records it on the match.
- Ask to go live when it works. A V-ENT admin reviews the game first.
The sandbox and test keys
A sandbox game reaches only tournaments your own account runs and the testers you name, so you can build against real brackets without touching anybody else's. Make a test key in the portal: it starts with the prefix below and reaches sandbox games only. A live key reaches live games only.
Authorization: Bearer vent_pk_test_<key id>.<secret>Every request is signed, both ways
Every request V-ENT sends you, and every update you post to us, is signed with HMAC SHA-256 under your game's signing secret, over the method, the path and query, a timestamp, the host and a hash of the body. Refuse anything older than the window below, and keep each request id you have seen for that long so the same request cannot be played twice.
{METHOD}\n{path and query}\n{timestamp};{host};{base64 SHA-256 of the body}V-Ent-Timestamp- When the request was signed, in unix seconds
V-Ent-Content-SHA256- The base64 SHA-256 of the body
V-Ent-Signature- v1= and the signature, once per secret that is live
V-Ent-Delivery- A request id used once; keep it for the window to refuse repeats
- Window
- 300 seconds either side of now
After you rotate the secret, the old one keeps working for 24 hours and each request carries one v1= signature per secret, so you can swap without dropping anything.
import base64, hashlib, hmac, time
def verify(secret, method, path_and_query, host, headers, body):
stamp = int(headers["V-Ent-Timestamp"])
if abs(time.time() - stamp) > 300:
raise ValueError("stale")
content = base64.b64encode(hashlib.sha256(body).digest()).decode()
if not hmac.compare_digest(content, headers["V-Ent-Content-SHA256"]):
raise ValueError("body changed")
text = f"{method}\n{path_and_query}\n{stamp};{host};{content}"
expected = base64.b64encode(
hmac.new(secret.encode(), text.encode(), hashlib.sha256).digest()).decode()
offered = [p.strip()[3:] for p in headers["V-Ent-Signature"].split(",")]
if not any(hmac.compare_digest(expected, o) for o in offered):
raise ValueError("bad signature")import crypto from 'node:crypto';
export function verify(secret, method, pathAndQuery, host, headers, body) {
const stamp = Number(headers['v-ent-timestamp']);
if (Math.abs(Date.now() / 1000 - stamp) > 300) throw new Error('stale');
const content = crypto.createHash('sha256').update(body).digest('base64');
if (content !== headers['v-ent-content-sha256']) throw new Error('body changed');
const text = `${method}\n${pathAndQuery}\n${stamp};${host};${content}`;
const expected = crypto.createHmac('sha256', secret).update(text).digest('base64');
const offered = headers['v-ent-signature'].split(',').map(p => p.trim().slice(3));
const ok = offered.some(o => o.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(o), Buffer.from(expected)));
if (!ok) throw new Error('bad signature');
}What V-ENT sends you
Every request has the same body, so one address can take them all. Game session calls go to your game session address; events go to your events address, or to the session address if you left that empty.
{
"id": "dl_8f2c...",
"type": "event",
"event": "result_confirmed",
"created_at": "2026-10-17T18:04:11Z",
"game": "<your game>",
"environment": "live",
"data": { }
}eventEventcreate_game_sessionCreate a game sessioncancel_game_sessionCancel a game sessionget_game_sessionAsk for a game session
Answer with any 2xx and the request is delivered. Anything else, or no answer within 10 seconds, is tried again on the schedule below and then given up on. Answer 410 to stop at once. Every request and what your server answered is in the delivery log on your game's page, and by key at /api/v1/studio/games/<game>/deliveries/.
Waits between tries for an event, in seconds: 30, 120, 600, 1,800, 3,600, 10,800, 21,600, 43,200, 86,400
Game sessions
A best of three is up to three game sessions, one per game. When a match is ready we send create_game_session with both lineups (with each player's game account when they linked one), the settings the organiser chose and an idempotency key. Treat a repeated key as the same lobby: we call again until you answer created or later.
{ "session_id": "your-lobby-42", "state": "created",
"join": { "room_code": "ROOM42", "room_password": "pw" } }A room_code in join is shown to both sides in their match room, unless they typed one of their own.
States move forward only: creating, created, starting, started, finished, or cancelled. Post every change, signed, to the callback address the create call gave you. A game that crashes is cancelled without a result, and V-ENT opens the next attempt of the same game, up to three.
curl -X POST https://api.v-ent.co/api/v1/studio/sessions/gs_<ref>/ \
-H "Authorization: Bearer vent_pk_<key id>.<secret>" \
-H "V-Ent-Timestamp: <unix seconds>" \
-H "V-Ent-Content-SHA256: <base64 sha256 of the body>" \
-H "V-Ent-Signature: v1=<signature>" \
-H "V-Ent-Delivery: <a new id for every request>" \
-d '{"state": "finished", "result": { }}'If a started game goes quiet for five minutes we ask you with get_game_session; answer it with the same body you would post. When a match is settled another way we send cancel_game_session. We never cancel a finished game.
Results and scoring
A result has two lineups, one for side 1 and one for side 2, an optional list of competitors (each with a side), and an optional general object. Inside that, the fields are yours, checked against the result schema you set on your game.
{
"lineups": [ { "side": 1, "score": 13, "won": true },
{ "side": 2, "score": 7, "won": false } ],
"competitors": [ { "account_id": "player-one", "side": 1, "kills": 9 } ],
"general": { "map": "Bermuda" }
}Schema keywords V-ENT checks: type, properties, required, items, enum, minimum, maximum, minLength, maxLength, minItems, maxItems, additionalProperties, description, title, $schema. A schema using any other keyword is refused when you save it, rather than half checked.
Scoring expressions
Your expression turns one side's raw result into its score for that game. It is worked out once per side, and the answer must be a whole number from 0 to 999.
team.score
competitors.sum(it.kills)
team.roundsWon + competitors.sum(it.kills)
global.double ? team.roundsWon * 2 : team.roundsWon
settings.mode == 'KillRace' ? competitors.sum(it.kills) : (team.won ? 1 : 0)- Names
team, opponent, competitors, global, settings- Functions
abs, ceil, floor, max, min, round- On a list
.all(it...), .any(it...), .avg(it...), .count(it...), .max(it...), .min(it...), .sum(it...)
The expression is read by V-ENT's own small parser, never run as code. Try one against a sample result on your game's page before you save it.
The result reaches the match through the same rules as a result typed by hand. Anything those rules refuse, such as a level knockout game, waits for the organiser, who keeps the final say.
Linking player accounts
With a code: your server asks for one for its account, your game shows it, and the player types it at v-ent.co/link-game while signed in. They see your game and their account name before confirming. Codes last ten minutes and are used once. Ask whether it was used by its code_ref, never by the code.
curl -X POST https://api.v-ent.co/api/v1/studio/games/<game>/link-codes/ \
-H "Authorization: Bearer vent_pk_<key id>.<secret>" \
-d '{"account_id": "your-account-id", "display_name": "Ada#1"}'With Sign in with V-ENT: sign the player in from your game, then post the access token with your account id to /api/v1/studio/games/<game>/links/. The token must be your own and carry the identity scope.
Events
The same list V-ENT's own overlays and run of show read. Choose which to receive on your game's page; choose none to receive them all. A test event can be sent from the page at any time.
result_confirmed- A match result was recorded
stage_started- A stage started
stage_closed- A stage finished
line_sent- Places were sent on to another stage
tournament_won- A tournament was won
account_linked- A player linked their account
account_unlinked- A player's link was removed
Reading for your own screens
Show a tournament inside your game with the read API: the stages and their standings (the same ranking the site shows), and a linked player's next match. Every answer says how much of your rate limit is left, so you can back off before you are refused.
GET /api/v1/tournaments/<slug>/structure/
GET /api/v1/tournaments/<slug>/standings/
GET /api/v1/studio/games/<game>/accounts/<account_id>/next-match/
X-Rate-Limit-Limit: 60
X-Rate-Limit-Remaining: 59
X-Rate-Limit-Reset: 42From the first game session, your game's page counts linked players who played each month and the game sessions played. Nothing is charged for it today.