BULLETMETA

Developers

A typed API over servers that count toward stats, their matches, players, boards and clans — plus your own organization's records, and the two lists your own tooling has to keep in step: the whitelist and the ban list. Available on the Enterprise plan.

Quickstart

Every method is a POST of a JSON body to /package.Service/Method on https://api.bulletmeta.app. That is the Connect protocol; the same endpoints speak gRPC and gRPC-Web.

curl https://api.bulletmeta.app/bulletmeta.api.v1.ServerService/GetLiveStatus \
  -H "Authorization: Bearer $BULLETMETA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"serverId": "…"}'

Streaming needs a real gRPC client — with buf curl:

buf curl --protocol grpc --http2-prior-knowledge \
  -H "Authorization: Bearer $BULLETMETA_API_KEY" \
  -d '{"serverId": "…"}' \
  https://api.bulletmeta.app/bulletmeta.api.v1.ServerService/WatchLiveStatus

Generated SDKs come from the schema on the Buf Schema Registry:

# Go
go get buf.build/gen/go/bulletmeta/api/connectrpc/go

# TypeScript
npm i @buf/bulletmeta_api.connectrpc_es

API keys

A key belongs to one organization and is created by its owner under Settings → API. The token bm_live_<prefix>_<secret> is shown once and only its SHA-256 is stored, so a lost key is replaced rather than recovered. Up to 25 active keys, expiry from 1 to 730 days, and a revocation takes effect within 60 seconds.

Authorization: Bearer bm_live_…

Scopes

ScopeWhat it opens
public:readEverything except OrgService, plus your own organization's servers and their matches.
org:readOrgService reads: sanctions, whitelist, audit log, members, playtime and seeding, seeding standings.
lists:writeOrgService writes: whitelist grants, bans, sanction evidence, finalize and lift.

There is no organization parameter anywhere: a key acts on the organization it belongs to. Anything it may not see answers not_found, which is indistinguishable from something that does not exist.

Rate limiting

10 requests a second per key, bursting to 100. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset; over the limit is HTTP 429 with Retry-After in seconds.

for attempt in 1 2 3 4 5; do
  sleep $(( 2 ** attempt ))   # and honour Retry-After when it is larger
done

Pagination

List methods take pageSize and answer with nextPageToken. Send it back as pageToken for the next page; an empty token is the end. Tokens are opaque keyset cursors — do not parse or build one.

Errors

Errors are Connect codes, with the HTTP status a Connect client maps them to:

unauthenticated (401)Missing, malformed, expired or revoked key.
permission_denied (403)The key lacks the scope this method needs.
not_found (404)No such record, or one this key may not see.
invalid_argument (400)A field is missing or out of range.
failed_precondition (412)The record is not in a state that allows this, such as a finalized sanction.
resource_exhausted (429)Rate limited; retry after Retry-After.
unavailable (503)A dependency is down. Retry with backoff.

Idempotent writes

Every write takes a requestId. Retrying with the same one returns the first response instead of writing again, for 24 hours — so a timeout you did not see the answer to is safe to repeat.

curl https://api.bulletmeta.app/bulletmeta.api.v1.OrgService/CreateWhitelistGrant \
  -H "Authorization: Bearer $BULLETMETA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"subject": "76561198000000000", "requestId": "seed-2026-09-16-01"}'

A write that was saved but could not be pushed to a game server answers with warnings and no error: the record exists, and retrying would write it twice. Every write is audited as your key, and shows in the console's audit log as "API key ‹name›".

Versioning

The schema is bulletmeta.api.v1, published on buf.build/bulletmeta/api. Fields are added, never renumbered or removed — every change runs through buf breaking. A breaking change would be a new package, never an edit to v1.

Reference

ClanService

Clans' public pages.

POST/bulletmeta.api.v1.ClanService/GetClan

Request

slugstring

Response

clanClan

POST/bulletmeta.api.v1.ClanService/ListMembers

Request

slugstring
pageSizeinteger
pageTokenstring

Response

membersClanMember[]
nextPageTokenstring

LeaderboardService

The public boards, as bulletmeta.app/stats ranks them.

POST/bulletmeta.api.v1.LeaderboardService/GetBoard

Request

categoryBoardCategory
pageSizeinteger1–100; 0 means 50. The board holds the top 1000.
pageTokenstring

Response

entriesBoardEntry[]
nextPageTokenstring

MatchService

Finished matches on servers visible to the key.

POST/bulletmeta.api.v1.MatchService/GetMatch

Request

matchIdstring

Response

matchMatch
resultsMatchResult[]

POST/bulletmeta.api.v1.MatchService/ListMatchKills

Oldest first, as a timeline.

Request

matchIdstring
pageSizeinteger1–500; 0 means 200.
pageTokenstring

Response

killsKill[]
nextPageTokenstring

POST/bulletmeta.api.v1.MatchService/ListMatches

Newest first.

Request

serverIdstringOnly this server's matches, when set.
pageSizeinteger
pageTokenstring

Response

matchesMatch[]
nextPageTokenstring

OrgService

The key's own organization. Reads need the `org:read` scope and writes need `lists:write`; there is no organization parameter on either, because a key only ever touches the organization it belongs to. Every write is audited as the key itself, and takes a `request_id` so a retried call returns the first result instead of writing twice.

POST/bulletmeta.api.v1.OrgService/AddSanctionEvidence

Request

sanctionIdstring
evidenceUrlsstring[]
requestIdstring

Response

sanctionSanction

POST/bulletmeta.api.v1.OrgService/CreateBan

Request

steamIdstring
serverIdstringUnset bans on every server of the organization.
reasonstring
notesstring
evidenceUrlsstring[]Without evidence the ban applies but stays open ("needs evidence").
expiresAtgoogle.protobuf.Timestamp
requestIdstring

Response

sanctionSanction
warningsstring[]

POST/bulletmeta.api.v1.OrgService/CreateWhitelistGrant

Writes (`lists:write`).

Request

subjectstringA SteamID64 or a BulletMeta profile id. The profile is created for an unknown SteamID64, exactly as a join would create it.
serverIdstringUnset grants on every server of the organization, now and future.
notestring
expiresAtgoogle.protobuf.TimestampUnset is open-ended. The game has no expiry, so BulletMeta enforces one by leaving the SteamID out of the next push.
requestIdstringRepeat a call safely: the same id returns the first call's grant.

Response

grantWhitelistGrant
warningsstring[]Servers the push did not reach. The grant is written either way.

POST/bulletmeta.api.v1.OrgService/FinalizeSanction

Request

sanctionIdstring
requestIdstring

Response

sanctionSanction

POST/bulletmeta.api.v1.OrgService/LiftSanction

Request

sanctionIdstring
reasonstring
requestIdstring

Response

sanctionSanction
warningsstring[]

POST/bulletmeta.api.v1.OrgService/ListAuditLog

Request

pageSizeinteger
pageTokenstring

Response

entriesAuditEntry[]
nextPageTokenstring

POST/bulletmeta.api.v1.OrgService/ListOrgMembers

Request

pageSizeinteger
pageTokenstring

Response

membersOrgMember[]
nextPageTokenstring

POST/bulletmeta.api.v1.OrgService/ListPlayerPresence

Playtime and seeding time per player, for the key's own servers.

Request

pageSizeinteger
pageTokenstring
serverIdstringOne of the key's own servers; unset covers every one of them.
fromgoogle.protobuf.Timestamp
togoogle.protobuf.Timestamp

Response

playersPlayerPresence[]
nextPageTokenstring

POST/bulletmeta.api.v1.OrgService/ListSanctions

Request

pageSizeinteger
pageTokenstring

Response

sanctionsSanction[]
nextPageTokenstring

POST/bulletmeta.api.v1.OrgService/ListSeedingStandings

Where players stand under the organization's seeding whitelist rules.

Request

pageSizeinteger
pageTokenstring
ruleIdstringOne rule; unset covers every rule of the organization.

Response

standingsSeedingStanding[]
nextPageTokenstring

POST/bulletmeta.api.v1.OrgService/ListWhitelist

Request

pageSizeinteger
pageTokenstring
includeRevokedbooleanInclude revoked grants.

Response

grantsWhitelistGrant[]
nextPageTokenstring

POST/bulletmeta.api.v1.OrgService/RevokeWhitelistGrant

Request

grantIdstring
requestIdstring

Response

grantWhitelistGrant
warningsstring[]

PlayerService

Player careers, from counted matches on enrolled servers.

POST/bulletmeta.api.v1.PlayerService/GetCareerStats

Request

playerPlayerSelector

Response

statsCareerStats

POST/bulletmeta.api.v1.PlayerService/GetPlayer

Request

playerPlayerSelector

Response

playerPlayerRef
createdAtgoogle.protobuf.Timestamp

POST/bulletmeta.api.v1.PlayerService/GetPlaytime

Request

playerPlayerSelector
serverIdstringOnly this server's sessions, when set.
organizationIdstringOnly sessions on this organization's servers, when set.

Response

totalSecondsintegerstringThe sum over `servers`.
onlineboolean
lastConnectedAtgoogle.protobuf.Timestamp
serversServerPlaytime[]Most played first.

POST/bulletmeta.api.v1.PlayerService/GetRatingHistory

Request

playerPlayerSelector
pageSizeinteger1–500; 0 means 100. Newest first.
pageTokenstring

Response

pointsRatingPoint[]
nextPageTokenstring

POST/bulletmeta.api.v1.PlayerService/GetWeaponStats

Request

playerPlayerSelector

Response

weaponsWeaponStat[]

POST/bulletmeta.api.v1.PlayerService/ListPlayerMatches

Request

playerPlayerSelector
pageSizeinteger
pageTokenstring

Response

matchesPlayerMatch[]
nextPageTokenstring

ServerService

Game servers: enrolled (public) servers, plus the key's own organization's.

POST/bulletmeta.api.v1.ServerService/GetLiveStatus

The live tile: status, map, players, scores. No roster.

Request

serverIdstring

Response

liveStatusLiveStatus

POST/bulletmeta.api.v1.ServerService/GetServer

Request

serverIdstring

Response

serverServer

POST/bulletmeta.api.v1.ServerService/ListServers

Servers visible to the key, by name.

Request

pageSizeinteger1–100; 0 means 25.
pageTokenstring

Response

serversServer[]
nextPageTokenstring

STREAM/bulletmeta.api.v1.ServerService/WatchLiveStatus

Server stream: the server's live tile, then every update as it is published. gRPC, gRPC-Web or Connect streaming — not plain JSON over POST.