Bitskiff

REST API

On this page

Everything an SDK does is an HTTP call you can make yourself, and every one of them is on this page.

Some names on this wire are not the words the rest of the site uses.

On the wireThe word this site uses
participants, participantId, the participant-state values they carry, the participant_minutes meter, ParticipantView, ParticipantStats, ParticipantMetersclient
seat, seatClass, seatDeadlineMs, the seat_spent error codea client's place
the "admin" value of a key's type, the admin_key audit actor, the /admin-keys routesorganization key

Base URL

Every path hangs off one address, and each environment has its own. This site was built for dev, whose API answers on https://api.dev.bitskiff.com. A key, a module or a CLI built for one environment talks only to that one. Every path is versioned under /v1, and a 64-bit number travels as a string.

A request that asks for JSON gets JSON: send Accept: application/json and both bodies are the JSON on this page. Accept decides. With no Accept the answer is encoded the way the request body was, and a request that carries neither is answered in protobuf.

Bodies

These bodies are protobuf messages written out as JSON, and one rule of that writing shows through. A field at its zero value is left out of the JSON entirely. A false, a 0, an empty string and an empty map never appear, so a field that is absent and a field at its zero value say the same thing.

A list is the exception: every repeated field is present, and an empty one is []. So GET /v1/sessions on a project with nothing open answers {"sessions": []}, and reading a list off any answer on this page is safe without a guard.

A few fields carry presence of their own, and for those a zero and an absent field are two different answers. The limits on a key and the fields of a patch body work that way: a 0 is a ceiling of zero, and leaving the field out is what asks for no change.

So do hostResumeWindow and warningLead on a session's config: a 0s asks for no host grace and no warning, and leaving the field out takes the project default.

The examples are abridged. Each one shows the fields worth reading for that call; a live answer carries the rest of the object as the tables here describe it.

Credentials

Every request carries one credential, and its prefix says which class it is. The class decides which routes it reaches at all; the permissions on the key or token decide the rest. The last two rows are what a route asks for rather than credentials of their own.

What a route asks forWhat it is
publishable keyStarts bsk_pk_. Safe to ship in a page, because it works only from an origin you allowed. It opens sessions and does nothing else.
secret keyStarts bsk_sk_. For your own backend or a device you control. A request carrying an Origin header is refused, so it cannot be used from a browser.
tokenStarts bsk_at_. What a secret or organization key mints for one user or device, for a while. Hand it to a page or an app that is already behind your own sign-in.
organization keyStarts bsk_ak_. The management credential: projects, keys, limits, settings and the audit log, across an organization.
host tokenStarts bsk_ht_. Handed to the host when the session opens. It gets the host back into its own session after a reload or a restart.
resume tokenStarts bsk_rt_. Held by a client so it can return to the session it already joined.
console sign-inStarts bsk_cs_. What the console holds after you sign in. It opens no session, and an integration never holds one.
manageNot a credential of its own: what may administer one running session. A host token, a resume token whose client holds manage, or a secret key, organization key or token carrying the sessions:manage permission.
no credential12 routes need none: the two a client calls with a join code, the two sign-in-link routes, and the two that say what this endpoint is.

Headers

Six headers, and a credential never travels in a path, a query string or a fragment.

HeaderWhat it carries
AuthorizationBearer <credential>. The one header every credential travels in.
Acceptapplication/json to get the JSON on this page. It is the header that decides. Leave it off and the answer is encoded the way the request body was, so a request sent with Content-Type: application/json is answered in JSON; leave both off and the answer is protobuf.
Content-Typeapplication/json on every request that carries a body, and the one header that makes the bodies on this page sendable as they are written. Any type but application/json or application/x-protobuf is refused validation with the message unsupported request media type and details.header Content-Type. Leaving it off leaves the body unread, so a route that needs one refuses validation. A GET or a DELETE carries no body and needs no Content-Type.
X-Bitskiff-ProjectThe project slug, such as acme-demo. Send it on every project-scoped route when the credential is an organization key or a console sign-in, either of which can serve more than one project. A proj_ id is still accepted where one is already in use.
Idempotency-KeyOptional, on POST /v1/sessions. 16 to 128 printable ASCII characters of your own randomness; anything else is refused validation with details.header Idempotency-Key, and so is the header itself on a publishable key that carries no token. Sent again while the first open is still running it is refused idempotency_in_flight; sent again with the same body inside the replay window it answers that first session rather than opening a second one. The window is 24 hours when the call carries a key, and 60 seconds when it carries a token: a token-backed retry later than a minute opens a second session.
X-Bitskiff-SDKOptional. Which SDK is calling and its version: ts-host/<semver> from a host, ts-client/<semver> from a client, such as ts-client/0.1.0. The SDKs send it themselves, and the routes with an sdk field carry the same value in the body.

Which permission a credential needs is on the route itself, under Permission. Credentials says what each name covers and which class of credential may hold it.

HTTP
POST /v1/sessions
Authorization: Bearer bsk_ak_...
Accept: application/json
Content-Type: application/json
X-Bitskiff-Project: acme-demo

Errors

Every response that is not a 2xx is the same object.

FieldWhat it carries
codeThe one field to branch on. Every value it takes is in the table at the end of this page.
messageA sentence for a person. Never parse it.
retryableWhether sending this same request again can work.
retryAfterMsHow long to wait first. The Retry-After header carries the same wait in seconds.
requestIdThe handle support traces a refusal by. Also the X-Bitskiff-Request-Id response header.
closeCodePresent only on a refusal that ended a session or a client: the code it was closed with. A read carries none, whatever it refuses.
detailsWhat the refusal was about, as string keys. Every key it can carry is in the table under this one, and every value reason takes in the table after that.

The zero-value rule under Bodies holds here too: retryable: false, retryAfterMs: 0, closeCode: 0 and an empty details map never appear.

Every key details can carry. A refusal carries the ones its own rule has an answer for and no others, so read a key only when it is there.

details keyTypeWhat it carries
fieldstringThe JSON path of the request field at fault, in the spelling the caller sent it, or the query parameter's name on a GET: limit and cursor on a paged route, subject, hostState, kind, since, until.
optionstringThe SDK option at fault, in the spelling the developer writes it and dotted where it is nested (reconnect.base), on a refusal an SDK raises locally before any request goes out.
headerstringThe request header at fault, in its canonical spelling: Content-Type, Accept, Idempotency-Key, X-Bitskiff-Project, X-Bitskiff-Key.
pathstring<METHOD> <path> as it was sent, on the not_found for a path this service does not carry and on the 405 for a documented path asked for with a method it does not answer.
namedstringWhat an out-of-reach read asked for, on the one not_found step 4.
sessionIdstringThe session id the request named, as it was sent, on session_not_found.
reasonstringWhich rule refused, from the vocabulary in the table below.
classstringThe credential classes this route DOES admit, in the words teaches (organization key or console sign-in)—not the class that was presented, which the caller already knows: the refusal is there to say what would work.
credentialstringThe prefix of the credential that was presented (bsk_pk_), never the credential itself.
permissionstringThe permission the route asked for and the credential does not hold, on a missing_permission refusal.
objectstringThe resource inside the caller's own scope that it may not touch, as <type>:<id> (project:proj_...): `, and only for something the caller can already reach. |
keyIdstringThe key a refusal is about, on the own_line and not_descendant refusals.
rolestringThe organization role a key may not hand out, on a role_above_key refusal.
policystringThe organization policy that refused the act, on a policy refusal: orgKeyMinting or deviceSignIn.
expiredAtunixMsWhen the organization's plan expired, Unix milliseconds, on plan_expired to the developer; a page never receives it.
supportedstringThe protocol versions this service does speak, on protocol_unsupported.
ceilingintegerA console-plane ceiling on a validation refusal, as well as the limit key of the table above.

Every value details.reason takes. One vocabulary for every code that carries it, so a branch on the reason is a branch on the rule that refused rather than on a sentence.

details.reasonOnWhat it says
absentinvalid_key, origin_not_allowedNothing was presented: no Authorization: Bearer at all, or on the join routes no client key in X-Bitskiff-Key and no token (details.header names X-Bitskiff-Key) outside the sample client page's keyless case; or a host key with no Origin header, which is a caller that is not a browser, or a client key with neither an Origin header nor an origin declaration.
malformedinvalid_keyA credential was presented and carries none of the prefixes.
unknowninvalid_key, invalid_token, handoff_invalidWell-formed and this deployment holds no such record: a key of another deployment, a truncated one, or a single-use sign-in credential that was never minted, is already spent or has been superseded—a device code included, since redeeming one deletes it.
revokedinvalid_key, invalid_tokenThis deployment issued it and it has been revoked; mint a fresh one.
spenthandoff_invalidThe handoff ticket was already redeemed once, or a final refusal naming it spent it; the app shows one line and its scanner.
supersededhandoff_invalidThe page minted a later handoff ticket for the same place, and one is pending at a time; the later ticket is the live one.
cancelledhandoff_invalidThe page cancelled the handoff before this redeem (DELETE .../clients/{clientId}/handoff); the place stays the page's.
lapsedhandoff_invalidOn a resume by the app: the handoff it redeemed never committed, because the window ended before its leg attached, so the place stayed the page's and the app's stored record is deleted.
expiredinvalid_key, invalid_token, handoff_invalidPast its expiresAt; mint a fresh one.
mismatchorigin_not_allowedThe Origin the browser sent is not on this publishable key's allowlist, or, on a client key and only when the request carries no Origin header or Origin: null, the body's origin declaration is not.
other_projectwrong_clientThe session belongs to another project than the client key's; raised at inspect and join before any join grant or ticket, and naming nothing else.
other_hostwrong_clientThe session's opener key is not one of the host keys the client key serves; raised at inspect and join before any join grant or ticket, and naming nothing else.
not_for_classforbiddenThe credential is well-formed and its class may not call this route; details.class names the classes that may.
missing_permissionforbiddenThe class admits the route and the effective permission list does not carry the word; details.permission names it.
own_lineforbiddenA key acted on itself or an ancestor, which is what stops a leaked key rotating the key its owner would stop it with.
not_descendantforbiddenA key rotated or revoked an organization key that does not descend from it: a console-made key, a sibling's, the login key on another machine; details.keyId names the target.
policyforbiddenAn organization policy refuses the act: orgKeyMinting: console on a key making or rotating an organization key, deviceSignIn: refused on a console sign-in a device approval issued (bitskiff login, init), the login key's mint among its requests, and on every request of a login key, whenever it was minted; details.policy names the policy.
project_archivedforbiddenThe project is archived, so it opens no session, creates or rotates no key and mints no token; details.object names it, and an owner can unarchive it within 30 days of archiving.
unarchive_window_closedforbiddenThe project was archived more than 30 days ago, so it can no longer be unarchived; details.object names it.
role_above_keyforbiddenAn organization key invited a role above its own, or changed or removed a member who holds one; details.role names it.
own_roleforbiddenA console user tried to change their own organization role; another admin or owner changes it.
other_appwrong_clientOn POST /v1/handoff: the client key is this project's but not the registered app's for it; only the registered app's client key redeems a handoff ticket.
place_not_attachedforbiddenOn POST .../clients/{clientId}/handoff: a handoff ticket is minted only for a place whose record at the service reads attached, present and not released, whatever transport the page is on.
e2ee_sessionforbiddenOn POST .../clients/{clientId}/handoff: no handoff in an end-to-end encrypted session joined without a token, because the app's new key would need a re-confirmation for a place that already exists, which is not designed yet; under the authenticated join the token's cnf vouches for the app's key and the handoff is allowed.
token_requiredforbiddenThe require_token row, the project's word clamped by the opening key's, refused the join: a join with no token where it is token or token_bound_to_session, or a token that names no session where it is token_bound_to_session.
token_not_for_sessionforbiddenA join token bound to one session was presented at a join into another session of its project, whatever the require_token row says.
grant_expiredinvalid_codeThe join grant presented on POST /v1/join is past its joinGrantTtl or was never minted; re-entry is another scan.
grant_usedinvalid_codeThe join grant was already redeemed, and this is not a repeat of that redemption inside its 30 s replay window; re-entry is another scan.
grant_holder_mismatchinvalid_codeThe holder presented with the join grant does not hash to the holderHash its reservation carried, so the grant was copied and is not this page's.
nothing_openinvalid_codeA fixed code that exists in the client key's project and that no open session holds the claim of: the code is good and nothing is running here, which the reference join page shows as that and not as an error.
not_transferableforbiddenThe invitation was sent to another address.
device_code_deniedforbiddenThe person on the approval page pressed Cancel, so this device code will never sign anybody in; a terminal stops polling at once.
email_unverifiedforbiddenOn POST /v1/auth/sign-in/oidc/{provider}/callback: the provider does not vouch for the email (Google's email_verified or Microsoft's xms_edov is not true), so it never links and never creates a console user; sign in by an email link to that address instead.
sign_up_closedforbiddenOn POST /v1/auth/sign-in/oidc/{provider}/callback: a new address under identity.selfSignUp: allowlist that neither the sign-up allowlist nor an invitation admits; the caller has just proved they hold the address, so the refusal names its reason.
federated_link_requiredforbiddenOn POST /v1/auth/sign-in/oidc/{provider}/callback: a provider identity new to Bitskiff whose verified email is a console user on another Identity Platform account.
federated_identity_in_useforbiddenOn POST /v1/auth/sign-in/oidc/{provider}/callback: the provider's subject is linked to one console user, and the popup landed on another console user's Identity Platform account or on one its own user does not hold.
mfa_requiredforbiddenThe console user has no second factor and must enrol one: an organization owner or a member under mfaRequired past identity.mfaEnrolmentGraceS, or anyone on a step-up route.
mfa_step_upforbiddenThe console user has a second factor, but did not pass it within identity.stepUpWindowS; pass it again and retry once.
staff_access_refusedforbiddenA platform:mrc#staff read of an organization whose owner set staffAccess to refused; the staff lists count such an organization without naming it.
not_a_url, scheme, credentials, fragment, unresolvable, not_publicforbiddenWhich rule a webhook URL broke: the six are the whole of the policy.
durationvalidationA duration string is not one: it broke the grammar—a bare number, a space, a sign, an unknown or out-of-order unit, or a count whose parse is not whole milliseconds.
not_our_apiunavailableRaised by the SDK, never by the service: the answer came from something other than the service—a captive portal's page, a 302 or 511 from a network box, a 2xx without the service's content type—so it is a transport failure the SDK retries, and it never ends a session or deletes a stored credential.
last_ownervalidationRemoving or demoting this member would leave the organization with no owner; make another member an owner first.
last_defaultvalidationThe billing account is the organization's default and an organization keeps exactly one; make another billing account the default instead.
invoice_statusvalidationThe invoice's status does not allow that change: only an issued invoice is marked paid, and only an issued or paid one is voided; the message names the status it has.
month_not_endedvalidationAn invoice is issued for a calendar month (UTC) that has ended; name an earlier month.
max_members_per_orgvalidationThe organization is at its member ceiling; details.ceiling carries the number.
an end reasonsession_ended, seat_spentWhy the session or the seat is gone, from the reasons registry's session surface (host_closed, idle_timeout, max_session_time, reconnect_budget, background_budget): present when the record still says, absent when it does not.

A path no route on this page carries is refused not_found with a 404, the message no such route and details.path naming the method and path that was sent. A path outside /v1 answers the same object, so the base URL mistyped reads:

JSON
{
  "code": "not_found",
  "message": "no such route",
  "requestId": "3f1b6a52-9c4d-4f0a-8b21-6de0c2f4a917",
  "details": { "path": "GET /sessions" }
}

An id, slug or token the caller cannot reach is the same not_found, with the message nothing this caller can reach is named that and details.named carrying what was asked for. It is the same answer whether the thing exists or not: that is what keeps one caller from learning another's ids. A session id is read the same way as any other id, and no read carries a closeCode.

A body the route cannot use is refused validation. details.field is the JSON path of the field at fault, in the spelling you sent it, and the message says what is wrong with it:

JSON
{
  "code": "validation",
  "message": "the body field config.maxClients takes a number",
  "requestId": "3f1b6a52-9c4d-4f0a-8b21-6de0c2f4a917",
  "details": { "field": "config.maxClients" }
}

A field the route does not take is the same refusal, with the message the body carries a field this route does not take and the path after it. A body that is not JSON at all has no field to point at, so that one is body is not JSON with no details.

A credential that is present and is none of the prefixes under Credentials is refused invalid_key with the message this credential is not a key or token this service issued.

A request that sends no Authorization header, or sends one that is not Bearer, is the invalid_key that says this route needs a credential in Authorization: Bearer. The two are different sentences, so a truncated key does not read as a missing one.

A limit that was reached is refused rate_limited or quota_exceeded. details.limit names the limit, and the message names the same limit in the same spelling, which is the one this site uses everywhere:

JSON
{
  "code": "rate_limited",
  "message": "too many requests for sessionOpensPerMinute; wait and retry",
  "requestId": "3f1b6a52-9c4d-4f0a-8b21-6de0c2f4a917",
  "retryable": true,
  "retryAfterMs": 10000,
  "details": { "limit": "sessionOpensPerMinute", "scope": "project" }
}

Sending one Idempotency-Key twice with a different body is refused validation with the message this Idempotency-Key was used with a different request body and details.header naming the header rather than a field. A retry has to carry the body the first attempt did; a new request needs a new key.

Retry on retryable alone, after retryAfterMs. The clears column in the table at the end of this page is a separate fact, for a loop that keeps trying: a concurrency or rate refusal clears on its own, so the same call can simply be made again. A cap clears at the period roll or at a raise.

Paging

Seven routes answer a page at a time, and the contract is one contract. Send cursor with a limit; read nextCursor off the answer and send it back as the next cursor. The last page answers no nextCursor, so read until it stops coming rather than until a page is empty.

Sessions

Open a session, read it while it runs, and end it.

POST /v1/sessions

Open a session. Answers the session, the host token, the host's own ticket and the join code a client joins with.

Who may call it: A publishable key, or a secret key, organization key or token carrying sessions:open.

Permission: sessions:open, on a credential that carries permissions at all.

Request body

FieldTypeRequiredDescription
configSessionConfignoPartial; a field you leave out takes the project default. Every duration is a duration string—15s, 2m, 1h30m—and comes back as you wrote it, or in canonical form when a project ceiling moved it. hostResumeWindow and warningLead read 0s as a request rather than as silence: no host grace at all, no warning at all.
labelstringnoYour own name for this session, 64 characters at most; a longer one is refused validation with details.field label. It comes back on every read of the session and is what the console shows.
metadataobject, string keys to stringnoYour own keys and values, carried with the session and returned on every read. The service stores them and never reads them. At most 16 entries, refused validation with details.field metadata, and each value at most 256 bytes, refused with details.field naming metadata.<key>.
protocolProtocolRangenoThe protocol versions the calling SDK can speak. The service pins one for the life of the session and refuses protocol_unsupported when no version is in both ranges.
sdkstringnoSame value as X-Bitskiff-SDK.
channelsarray of ChannelRownoThe host's presented channel table: every row the host declared before open, ids assigned.
fixedCodeIdstringnoThe id of the fixed code this session claims, the host SDK's fixedCode open option; empty claims nothing.
originstringnoWhere a native build says it is: the custom scheme its host key lists, com.studio.racer://host.

Response body

FieldTypeDescription
sessionSessionViewThe session that was opened: its config as it was clamped, the ceilings it was clamped against, and its state.
hostTokenstringThe host's own credential for this session, returned here once. A host presents it to read and close the session, and to take a new host ticket after a reload.
hostTicketTicketThe host's own ticket, carrying the GRANT_RECEIVE_INPUT, GRANT_BROADCAST and GRANT_MANAGE grants.
joinCodeJoinCodeMaterialThe material a join code is computed from—sessionCode, secret, regionHint and the stepS it rotates on—and the joinUrl it rides on. The code itself is not in here: an SDK computes it locally, and over HTTP you compute it yourself, as The join code sets out. When the code next rotates is deadlines.codeRotatesAtMs on the session.
serverTimeMsstring (64-bit integer)The service's own Unix ms at the moment it answered, so every instant in this message is read against the clock that set it.
limitsSessionLimitsThe caps this session's clients are held to.
capacityCapacityNoticeThe project's standing against the caps this session's own usage spends, at the moment it opened. It is sent only when at least one of those caps has reached its warning point, 80% of its ceiling, or is spent; when they are all clear the field is left out, and an absent notice means exactly { status: "ok", limits: [] }. Guard the field before reading capacity.limits. The caps with a row here are clientMinutesPerDay (at the project and at the key that opened the session), monthlySpendCap, turnBytesPerMonth and samplePageSessions. concurrentSessions never has one: it refuses the next open rather than this session, and the refusal is where you read it.
channelsarray of ChannelRowThe effective channel table: the presented rows, defaults filled and clamped.
stunUrlsarray of stringBitskiff's STUN URLs: the relay's stun: and stuns: entries and no credential, the servers a peer offer carries.

Example request

JSON
{
  "config": {
    "maxClients": 2
  },
  "label": "lobby 14"
}

Example response

JSON
{
  "session": {
    "sessionId": "s_O2SX-CMRO6O-R6XoN4ywTv",
    "projectId": "proj_g1qr384an9asb4kdp10e",
    "state": "open",
    "label": "lobby 14",
    "region": "us-central1",
    "openedAt": "1774000000000"
  },
  "hostToken": "bsk_ht_...",
  "hostTicket": {
    "sessionId": "s_O2SX-CMRO6O-R6XoN4ywTv",
    "participantId": "h_0mFkpdUNnY5zgH5t3k7Qgv",
    "grants": [
      "GRANT_RECEIVE_INPUT",
      "GRANT_BROADCAST",
      "GRANT_MANAGE"
    ],
    "opener": true,
    "expiresAt": "1774000060000"
  },
  "joinCode": {
    "sessionCode": "AbCdEfG",
    "joinUrl": "https://example.com/join#",
    "stepS": 30
  },
  "serverTimeMs": "1774000000000"
}

POST /v1/sessions/{sessionId}/host-ticket

Give the host its place back after a reload or a restart, and rotate its host token.

Who may call it: A host token.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
protocolProtocolRangenoThe protocol versions the calling SDK can speak. The service pins one for the life of the session and refuses protocol_unsupported when no version is in both ranges.
sdkstringnoWhich SDK is calling and its version, such as ts-host/0.1.0. It is carried on the session's own rows.
channelsarray of ChannelRownoA resume presents the same table the open did: the service clamps it under the session's key and compares the result to the session's effective table, field by field, ids included.
evictedEpochnumbernoSet only after the engine closed the host's connection because another connection used its identity: the host epoch that connection was minted under. The service answers replaced when a later host ticket has moved the epoch since, and otherwise resumes the host with replay set.
lostNodeLostNodenoSet only by a host whose link the custom engine ended with bye node_lost or server_shutdown: the process that bye named.

Response body

FieldTypeDescription
sessionSessionViewThe session this ticket is for: its config as it was clamped, the ceilings it was clamped against, and its state.
hostTicketTicketWhat the host connects its transport with.
hostTokenstringRotated; the predecessor stays good until the successor is first presented, and no clock ends it before that.
joinCodeJoinCodeMaterialThe material a join code is computed from—sessionCode, secret, regionHint and the stepS it rotates on—and the joinUrl it rides on. The code itself is not in here: an SDK computes it locally, and over HTTP you compute it yourself, as The join code sets out. When the code next rotates is deadlines.codeRotatesAtMs on the session.
serverTimeMsstring (64-bit integer)The service's own Unix ms at the moment it answered, so every instant in this message is read against the clock that set it.
limitsSessionLimitsRe-sent on every host ticket, so a host that resumes re-reads it.
channelsarray of ChannelRowThe session's effective channel table, answered when the presented table, clamped under the session's key, matched it field by field.
replaybooleanTrue when the request named an evicted epoch and it had not moved: the eviction was a replayed ticket, and this is the ordinary resume.
relocationRelocationPresent when the request carried lostNode and the session was re-placed, by this request or an earlier one; absent on every other answer.
stunUrlsarray of stringBitskiff's STUN URLs, as OpenSessionResponse.stunUrls`: re-sent on every host ticket, so a host that resumes re-reads them. |

Example request

JSON
{
  "protocol": {
    "min": 1,
    "max": 1
  },
  "sdk": "ts-host/0.2.0"
}

Example response

JSON
{
  "session": {
    "sessionId": "s_O2SX-CMRO6O-R6XoN4ywTv",
    "state": "open",
    "label": "lobby 14"
  },
  "hostTicket": {
    "sessionId": "s_O2SX-CMRO6O-R6XoN4ywTv",
    "participantId": "h_0mFkpdUNnY5zgH5t3k7Qgv",
    "opener": true,
    "expiresAt": "1774000360000"
  },
  "hostToken": "bsk_ht_...",
  "serverTimeMs": "1774000300000"
}

POST /v1/host-ticket

The same, for a host that holds the host token and no session id: the token says which session it is.

Who may call it: A host token.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

FieldTypeRequiredDescription
protocolProtocolRangenoThe protocol versions the calling SDK can speak. The service pins one for the life of the session and refuses protocol_unsupported when no version is in both ranges.
sdkstringnoWhich SDK is calling and its version, such as ts-host/0.1.0. It is carried on the session's own rows.
channelsarray of ChannelRownoA resume presents the same table the open did: the service clamps it under the session's key and compares the result to the session's effective table, field by field, ids included.
evictedEpochnumbernoSet only after the engine closed the host's connection because another connection used its identity: the host epoch that connection was minted under. The service answers replaced when a later host ticket has moved the epoch since, and otherwise resumes the host with replay set.
lostNodeLostNodenoSet only by a host whose link the custom engine ended with bye node_lost or server_shutdown: the process that bye named.

Response body

FieldTypeDescription
sessionSessionViewThe session this ticket is for: its config as it was clamped, the ceilings it was clamped against, and its state.
hostTicketTicketWhat the host connects its transport with.
hostTokenstringRotated; the predecessor stays good until the successor is first presented, and no clock ends it before that.
joinCodeJoinCodeMaterialThe material a join code is computed from—sessionCode, secret, regionHint and the stepS it rotates on—and the joinUrl it rides on. The code itself is not in here: an SDK computes it locally, and over HTTP you compute it yourself, as The join code sets out. When the code next rotates is deadlines.codeRotatesAtMs on the session.
serverTimeMsstring (64-bit integer)The service's own Unix ms at the moment it answered, so every instant in this message is read against the clock that set it.
limitsSessionLimitsRe-sent on every host ticket, so a host that resumes re-reads it.
channelsarray of ChannelRowThe session's effective channel table, answered when the presented table, clamped under the session's key, matched it field by field.
replaybooleanTrue when the request named an evicted epoch and it had not moved: the eviction was a replayed ticket, and this is the ordinary resume.
relocationRelocationPresent when the request carried lostNode and the session was re-placed, by this request or an earlier one; absent on every other answer.
stunUrlsarray of stringBitskiff's STUN URLs, as OpenSessionResponse.stunUrls`: re-sent on every host ticket, so a host that resumes re-reads them. |

Example request

JSON
{
  "protocol": {
    "min": 1,
    "max": 1
  },
  "sdk": "ts-host/0.2.0"
}

Example response

JSON
{
  "session": {
    "sessionId": "s_O2SX-CMRO6O-R6XoN4ywTv",
    "state": "open",
    "label": "lobby 14"
  },
  "hostTicket": {
    "sessionId": "s_O2SX-CMRO6O-R6XoN4ywTv",
    "participantId": "h_0mFkpdUNnY5zgH5t3k7Qgv",
    "opener": true,
    "expiresAt": "1774000360000"
  },
  "hostToken": "bsk_ht_...",
  "serverTimeMs": "1774000300000"
}

POST /v1/sessions/{sessionId}/resume

Come back to the place a client already holds, for as long as its reconnect budget lasts.

Who may call it: The client's own resume token.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
protocolProtocolRangenoThe protocol versions the calling SDK can speak. A resume is refused protocol_unsupported when the session's pinned version is outside this range.
sdkstringnoWhich SDK is calling and its version, such as ts-client/0.1.0.
evictedJtistringnoSet only after the engine closed this client's connection because another connection used its identity: the jti of the ticket that connection used. The service answers replaced when a later ticket was minted for this client since, and otherwise resumes it with replay set.
lostNodeLostNodenoSet only by a client whose link the custom engine ended with bye node_lost or server_shutdown: the process that bye named.
environmentClientEnvironmentnoAs JoinRequest.environment.
metadataobject, string keys to stringnoAs JoinRequest.metadata, read only beside a stated environment: a non-empty map replaces the place's, and an empty one keeps it, so a reload that restates nothing clears nothing.

Response body

FieldTypeDescription
ticketTicketWhat this client connects its transport with: a fresh one, for the connection it is making now.
resumeTokenstringToken rotated.
seatDeadlineMsstring (64-bit integer)Unix ms on the service's clock past which this resume token is refused and the client's place is gone.
serverTimeMsstring (64-bit integer)The service's own Unix ms at the moment it answered.
limitsSessionLimitsRe-sent on every resume, so a cap change reaches a live client.
policyDeadlineMsstring (64-bit integer)As JoinResponse.policyDeadlineMs, read again at the resume.
replaybooleanTrue when the resume named an evicted ticket and nothing was minted since: the eviction was a replayed ticket, and this is the ordinary resume.
handoffFailedstringOn an evicted page's resume only: the stage this resume failed the place's redeemed handoff at, crashed when the engine read found the successor's connection gone. One of HANDOFF_STAGE_MINTED, HANDOFF_STAGE_REDEEMED, HANDOFF_STAGE_CRASHED, HANDOFF_STAGE_REFUSED, HANDOFF_STAGE_CANCELLED.
registeredAppsarray of RegisteredAppThe native apps the project registered, as JoinResponse.registeredApps and from the same project read: a page that resumes a stored place on a fresh load, the page a client returns to through /return among them, learns them here and nowhere else.

Example request

JSON
{
  "protocol": {
    "min": 1,
    "max": 1
  },
  "sdk": "ts-client/0.2.0"
}

Example response

JSON
{
  "ticket": {
    "sessionId": "s_O2SX-CMRO6O-R6XoN4ywTv",
    "participantId": "p_MWnCKAv7YT4UCIX8ncPLUA",
    "expiresAt": "1774000360000"
  },
  "resumeToken": "bsk_rt_...",
  "seatDeadlineMs": "1774000960000",
  "serverTimeMs": "1774000300000"
}

POST /v1/sessions/{sessionId}/clients/{clientId}/handoff

A client on a page asks for a handoff ticket for its own place, so the project's registered app can take that place with no scan.

Who may call it: The client's own resume token.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
hintHandoffHintnoThe page's transport as it stands at the mint; absent when it states none.
dataTextstringnoThe handoff data as a string.
dataBytesstring (base64)noThe handoff data as bytes.
hostIdentityPublicstring (base64)noIn an end-to-end encrypted session, the raw 32-byte Ed25519 host identity key the page's own Welcome verified against its fingerprint: required there, validation when absent or another length, and empty in a plain session.

Response body

FieldTypeDescription
ticketstringThe handoff ticket, bsk_hf_ and 43 characters: carried in the handoff link's fragment only.
expiresAtMsstring (64-bit integer)Unix ms on the service's clock past which the ticket is refused handoff_invalid, expired.
serverTimeMsstring (64-bit integer)The service's own Unix ms at the moment it answered; the client reads expiresAtMs against it.

Example request

JSON
{
  "hint": {
    "transport": "peer",
    "grade": "good",
    "rttMs": 12
  },
  "dataText": "player-7"
}

Example response

JSON
{
  "ticket": "bsk_hf_...",
  "expiresAtMs": "1774000360000",
  "serverTimeMs": "1774000300000"
}

DELETE /v1/sessions/{sessionId}/clients/{clientId}/handoff

A client on a page withdraws the handoff it started, so its place stays where it is.

Who may call it: The client's own resume token.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

None. The service answers 204.

POST /v1/sessions/{sessionId}/clients/{clientId}/presence

A client says whether it is in the foreground or the background, and hears how much background time it has left.

Who may call it: The client's own resume token.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
presencestringyesWhat the client is now. A client is the only writer of its own presence, and its background budget runs from what it says here. One of PRESENCE_PRESENT, PRESENCE_AWAY.
jtistringnoThe jti of the newest ticket the client holds, from its join or its last resume. An away that names an older ticket comes from a page that has since reloaded and resumed, and changes nothing. Leave it empty to have the post read as the latest.

Response body

FieldTypeDescription
presencestringThe presence the service recorded. One of PRESENCE_PRESENT, PRESENCE_AWAY.
backgroundMsLeftnumberHow much of the background budget is left, in milliseconds. Zero once it is spent.
hostDeadlineMsstring (64-bit integer)The session's host grace as the service holds it when it answered: absolute Unix ms on the service's clock while the host is away, 0 while it is attached.

Example request

JSON
{
  "presence": "PRESENCE_AWAY"
}

Example response

JSON
{
  "presence": "PRESENCE_AWAY",
  "backgroundMsLeft": 28000
}

POST /v1/sessions/{sessionId}/clients/{clientId}/leave

A client says it is gone, so its place is freed without waiting for the connection to time out. Safe to send twice.

Who may call it: The client's own resume token.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
reasonstringnoWhy the client is going: host_unverified, or the empty string for an ordinary leave. Any other value is refused validation with details.field reason. It reaches the host as the reason on clientLeft.

Response body

FieldTypeDescription
presencestringThe presence the service recorded. One of PRESENCE_PRESENT, PRESENCE_AWAY.
backgroundMsLeftnumberHow much of the background budget is left, in milliseconds. Zero once it is spent.
hostDeadlineMsstring (64-bit integer)The session's host grace as the service holds it when it answered: absolute Unix ms on the service's clock while the host is away, 0 while it is attached.

Example request

JSON
{
  "reason": "host_unverified"
}

Example response

JSON
{
  "presence": "PRESENCE_PRESENT"
}

POST /v1/sessions/{sessionId}/clients/{clientId}/unheard

The host reports a client it holds as present that has sent nothing for 3 seconds. The host SDK sends this itself.

Who may call it: A host token.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
engineSidstringyesThe engine sid the host last heard this client on.
lastHeardMsstring (64-bit integer)noUnix ms on the host's own clock; the service logs it and decides nothing on it.
querybooleannoA question, not a report: the service commits nothing and answers where the record stands and the budget it is running.

Response body

FieldTypeDescription
detachedbooleanThis report committed the detach.
connectionstringThe record's connection after the report: "attached", "detached", "reserved" or "gone"; empty when there is no record.
budgetDeadlineMsstring (64-bit integer)The running reconnect or background budget of a detached record as the service holds it, and for a query of an attached one too: absolute Unix ms on the service's clock, 0 when none is running.

Example request

JSON
{
  "engineSid": "PA_7c2f1e9d",
  "lastHeardMs": "1774000003250"
}

Example response

JSON
{}

GET /v1/sessions/{sessionId}/clients/{clientId}/budget

The host asks where a client stands and which budget Bitskiff is running for it. The host SDK sends this itself.

Who may call it: A host token.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
connectionstringThe record's connection: "attached", "detached", "reserved" or "gone"; empty when there is no record.
budgetDeadlineMsstring (64-bit integer)The running reconnect or background budget of an attached or detached record as the service holds it: absolute Unix ms on the service's clock, 0 when none is running.

Example response

JSON
{
  "connection": "attached",
  "budgetDeadlineMs": "1774000125000"
}

POST /v1/sessions/{sessionId}/clients/{clientId}/release

The host releases a client's place: the client stops using its Bitskiff connection and the host serves it over another transport. The host SDK sends this itself.

Who may call it: A host token.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
transportstringyesThe name of the transport that carries the client for the span.
ifLoststringnoThe client's choice for a lost link, from its Release. One of RELEASE_IF_LOST_REJOIN, RELEASE_IF_LOST_END.

Response body

FieldTypeDescription
connectionstringThe record's connection after the call: "released" when the mark committed or was already there, "gone" for a place that is over.
releasedAtMsstring (64-bit integer)Absolute Unix ms on the service's clock the place was marked released.

Example request

JSON
{
  "transport": "wifi_direct",
  "ifLost": "RELEASE_IF_LOST_REJOIN"
}

Example response

JSON
{
  "connection": "released",
  "releasedAtMs": "1774000003250"
}

POST /v1/sessions/{sessionId}/clients/{clientId}/release-report

The host reports what became of a released client: its link was lost, or it ended. The host SDK sends this itself.

Who may call it: A host token.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
outcomestringyes"lost": the link died and the client rejoins, so the service detaches the place and arms its reconnect budget.
reasonstringnoFor "ended" only: "left" (the client's choice for a lost link, or its own leave) or "backgroundBudget" (its background budget, which the host runs for the span, ran out).
releasedAtMsstring (64-bit integer)noThe release this report is about, as ReleaseResponse answered it.

Response body

FieldTypeDescription
appliedbooleanThis report moved the place.
connectionstringThe record's connection after the report: "released", "detached", "attached" or "gone".
budgetDeadlineMsstring (64-bit integer)The reconnect budget a "lost" report armed: absolute Unix ms on the service's clock, 0 when none.

Example request

JSON
{
  "outcome": "lost",
  "releasedAtMs": "1774000003250"
}

Example response

JSON
{
  "applied": true,
  "connection": "detached",
  "budgetDeadlineMs": "1774000045000"
}

POST /v1/sessions/{sessionId}/host-release

The host lets its own Bitskiff connection go while every client it holds is released. The host SDK sends this itself.

Who may call it: A host token.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
releasedbooleanThe session is host_released after this call.
releasedAtMsstring (64-bit integer)Absolute Unix ms on the service's clock the session went host_released.
deadlinesSessionDeadlinesThe deadlines a released host holds on its own clock: max_time and the released ceiling.
blockingClientsarray of stringWhen not released: the held clients that are not.

Example response

JSON
{
  "released": true,
  "releasedAtMs": "1774000004000",
  "deadlines": {
    "maxTimeDeadlineMs": "1774014400000",
    "releasedCeilingDeadlineMs": "1774086404000"
  }
}

GET /v1/sessions/{sessionId}/clients/{clientId}/place

A client reads where its own place stands, so one that missed the host's release answer knows what to do. The client SDK sends this itself.

Who may call it: The client's own resume token.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
connectionstringThe record's connection: "reserved", "attached", "detached", "released" or "gone".
releasedAtMsstring (64-bit integer)When the place was marked released: absolute Unix ms on the service's clock; 0 unless released.
releasedTransportstringThe transport the host named at the release; empty unless released.

Example response

JSON
{
  "connection": "released",
  "releasedAtMs": "1774000003250",
  "releasedTransport": "wifi_direct"
}

POST /v1/sessions/{sessionId}/relay-credentials

The host asks for Bitskiff's relay servers and one credential for one direct-path offer. The host SDK sends this itself.

Who may call it: A host token.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
clientIdstringnoThe client the offer is for; empty is the host's own side.

Response body

FieldTypeDescription
urlsarray of stringThe relay's turn: URLs, udp and tcp.
usernamestring{expiry_unix}:{participantId}, naming no project and no key.
credentialstringBase64 HMAC-SHA1 of username under the relay's shared secret.
ttlSecondsnumberThe session's idle limit or 12 hours, whichever is shorter.

Example request

JSON
{
  "clientId": "p_MWnCKAv7YT4UCIX8ncPLUA"
}

Example response

JSON
{
  "urls": [
    "stun:relay.example.com:3479",
    "turn:relay.example.com:3479?transport=udp",
    "turn:relay.example.com:3479?transport=tcp"
  ],
  "username": "1774043500:p_MWnCKAv7YT4UCIX8ncPLUA",
  "credential": "...",
  "ttlSeconds": 43200
}

POST /v1/sessions/{sessionId}/clients/{clientId}/closing

A client whose page is closing says which connection is going. The client SDK sends this itself on pagehide.

Who may call it: The client's own resume token.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
closingSidstringyesThe engine sid of the connection the client is closing.

Response body

None. The service answers 204.

Example request

JSON
{
  "closingSid": "PA_7c2f1e9d"
}

Example response

JSON
{}

POST /v1/sessions/{sessionId}/stats

The host posts a minute of traffic, path and round-trip counters, its own and every client's.

Who may call it: A host token.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
windowStartstring (64-bit integer)yesUnix ms; start of the interval this report covers.
windowEndstring (64-bit integer)yesUnix ms.
seqnumberyesPer host process, keyed with the session epoch: increments once per report and starts at 1 again when a restarted host process resumes the session under a new epoch.
hostStatsnoThe host's own counters.
participantsarray of ParticipantStatsnoOne entry per client the host counted traffic for over this window.
controlarray of ControlCountnoThe control frames clients sent the host over this window, one entry per kind. A kind outside the control vocabulary is dropped, and a report naming more kinds than it has is refused validation with details.field control. The host's log, never a meter and never billed.

Response body

FieldTypeDescription
capacityCapacityNoticeThe project's standing again, read back after this report moved the counters, on the same rule as the open response: sent from the first cap at its warning point, left out while every cap is clear. This is how a live session learns inside 60 s that a cap is close, and a reader watching for a change treats an absent notice as { status: "ok", limits: [] }.

Example request

JSON
{
  "windowStart": "1774000000000",
  "windowEnd": "1774000060000",
  "seq": 1,
  "host": {
    "windowMs": 60000,
    "path": "unknown",
    "traffic": [
      {
        "topic": "in",
        "direction": "to_host",
        "path": "unknown",
        "messagesCount": "120",
        "bytes": "24000",
        "droppedCount": "0"
      }
    ],
    "rtt": {
      "windowCount": 60,
      "windowP50Ms": 18,
      "windowP95Ms": 42,
      "windowMaxMs": 61,
      "totalCount": "600",
      "totalSumMs": "11400"
    }
  },
  "participants": [
    {
      "participantId": "p_MWnCKAv7YT4UCIX8ncPLUA",
      "stats": {
        "windowMs": 60000,
        "path": "unknown"
      }
    }
  ]
}

Example response

JSON
{}

POST /v1/sessions/{sessionId}/close

End the session. Every client is disconnected, and the session comes back ended.

Who may call it: A host token, a resume token whose client holds manage, a secret key, organization key or token carrying sessions:manage, or a console sign-in.

Permission: sessions:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
reasonstringnoMax 128 chars; becomes ended.reason detail.

Response body

The SessionView object, written out under Objects.

Example request

JSON
{
  "reason": "the shift ended"
}

Example response

JSON
{
  "sessionId": "s_O2SX-CMRO6O-R6XoN4ywTv",
  "projectId": "proj_g1qr384an9asb4kdp10e",
  "state": "ended",
  "label": "lobby 14",
  "openedAt": "1774000000000",
  "endedAt": "1774000900000",
  "endedReason": "api_closed"
}

GET /v1/sessions

List the project's live sessions, paged.

Who may call it: A secret key, organization key or token carrying sessions:read, or a console sign-in.

Permission: sessions:read, on a credential that carries permissions at all.

Query parameters

These travel in the query string, not in a body.

FieldTypeRequiredDescription
subjectstringnoKeep only the sessions this subject opened. Omit it for every subject. A token reads its own subject and nothing else, whatever it sends here.
labelstringnoKeep only the sessions whose label is exactly this. It is how you read one host: the label a host opens with is its own name across every session its loop opens. Omit it for every label.
cursorstringnoWhere to carry on from: the nextCursor the page before this one answered with. It is opaque, and it belongs to this listing and this project: one this service did not answer with, or one from another listing, is refused validation with details.field cursor rather than answering an empty page.
limitnumbernoHow many sessions this page may answer with, 1 to 200. Omit it for 50. Every paged route takes the same bound, and a value outside it is refused validation with details.field limit rather than shrunk.
hostStatestringnoKeep only the sessions whose host is attached, or only those whose host is away and its host grace running. Anything else is refused validation. Omit it for every host state, which is the only way to see a session whose host has never attached.
clientSubjectstringnoKeep only the sessions with a client of this subject in them, matched exactly. The host's own place does not count. Omit it for every session. Under a token it narrows inside the token's own sessions.

Response body

FieldTypeDescription
sessionsarray of SessionViewOne session per row, as far as this page goes.
nextCursorstringWhere the next page starts. Send it back as cursor. It is absent when this page is the last, so read the pages until it stops coming rather than until it is empty.

Example query

HTTP
GET /v1/sessions?label=lobby%2014&limit=20

Example response

JSON
{
  "sessions": [
    {
      "sessionId": "s_O2SX-CMRO6O-R6XoN4ywTv",
      "projectId": "proj_g1qr384an9asb4kdp10e",
      "state": "open",
      "label": "lobby 14",
      "openedAt": "1774000000000",
      "controllersHeld": 1
    }
  ]
}

POST /v1/sessions/boot

Remove one subject from every session it is a client in, and free each place. Answers how many places were freed.

Who may call it: A secret key or organization key carrying sessions:manage, or a console sign-in.

Permission: sessions:manage, on a credential that carries permissions at all.

Request body

FieldTypeRequiredDescription
subjectstringyesThe subject to remove, matched exactly: the one its token was minted for. Every place it holds as a client in the project's open sessions is booted.
reasonstringnoWhy the subject is being booted, from the same list a single client's boot takes. It reaches each of its clients as the reason its session ended. One of BOOT_REASON_BOOTED, BOOT_REASON_IDLE, BOOT_REASON_POLICY, BOOT_REASON_KEY_UNVERIFIED, BOOT_REASON_RATE.

Response body

FieldTypeDescription
bootednumberHow many places this call freed. 0 when the subject held none, which is what a repeat answers.

Example request

JSON
{
  "subject": "user_8123",
  "reason": "BOOT_REASON_POLICY"
}

Example response

JSON
{
  "booted": 2
}

GET /v1/sessions/{sessionId}

Read one session and the clients in it.

Who may call it: A host token, a resume token whose client holds manage, a secret key, organization key or token carrying sessions:read, or a console sign-in.

Permission: sessions:read, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

The SessionView object, written out under Objects.

Example response

JSON
{
  "sessionId": "s_O2SX-CMRO6O-R6XoN4ywTv",
  "projectId": "proj_g1qr384an9asb4kdp10e",
  "state": "host_attached",
  "label": "lobby 14",
  "region": "us-central1",
  "openedAt": "1774000000000",
  "controllersHeld": 1,
  "participants": [
    {
      "participantId": "h_0mFkpdUNnY5zgH5t3k7Qgv",
      "opener": true,
      "seatClass": "opener",
      "seat": 1,
      "state": "connected",
      "presence": "PRESENCE_PRESENT",
      "joinedAt": "1774000000000"
    },
    {
      "participantId": "p_MWnCKAv7YT4UCIX8ncPLUA",
      "seatClass": "controller",
      "seat": 1,
      "state": "connected",
      "presence": "PRESENCE_PRESENT",
      "grants": [
        "GRANT_SEND_INPUT",
        "GRANT_RECEIVE_BROADCAST"
      ],
      "joinedAt": "1774000030000",
      "transportPath": "turn"
    },
    {
      "participantId": "p_Vkoa0Y7116thO-TASN8MNY",
      "seatClass": "controller",
      "seat": 2,
      "state": "gone",
      "presence": "PRESENCE_AWAY",
      "joinedAt": "1774000120000",
      "goneReason": "background_budget"
    }
  ]
}

GET /v1/sessions/{sessionId}/meters

Read the counters for a session and for each client in it. Nothing is written, and an ended session still answers for a while.

Who may call it: A host token, a resume token whose client holds manage, a secret key, organization key or token carrying sessions:read, or a console sign-in.

Permission: sessions:read, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
sessionIdstringThe session these meters were read from.
readAtMsstring (64-bit integer)When the counters were read, Unix ms on the service's clock. Every counter in this message is closed against this instant.
sessionobject, string keys to stringThe session's own counters, keyed the way the meter store holds them, and nothing else. How the session ran: epoch, seq, host_state, host_since_ms, attached_ms, away_ms, idle_ms. Who came and went: joins:attempted, joins:accepted, joins:refused, reconnects, resumes, grants_changes, host_attaches, host_resumes. How many at once: cur:{bundle}, peak:{bundle}, peak:participants. Time in state: pm:{bundle}:{state}, pg:{grant}:{state}. Traffic: msgs:{topic}:{dir}:{path}, bytes:{topic}:{dir}:{path}, turn:bytes_in, turn:bytes_out, turn:allocations, turn:grade; the {path} in those two keys is the path the service recorded, unknown where none was reported. The same traffic by the transport the host reported it on, host-reported and never billed: msgs:{topic}:{dir}:t:{transport}, bytes:{topic}:{dir}:t:{transport}; transports carries these summed. What the host reported: rtt:count, rtt:sum_ms, rtt:max_ms, stats_seq. Every value is a string: a count is a decimal string because these are 64-bit numbers, and a field that records a state or a name, such as host_state, carries that word.
participantsarray of ParticipantMetersOne entry for the host and one for each client, each with its own counters, keyed the same way and nothing else: state, bundle, grants, since_ms, joined_ms, ms:{state}, ms_bundle:{bundle}, ms_grant:{grant}, reconnects, grants_changes, resumed, device_class, os_family, network_class, country, path, path_since_ms, path_ms:{path}, path_changes, join_to_welcome_ms, turn:bytes_in, turn:bytes_out, turn:allocations, rtt:count, rtt:sum_ms, rtt:max_ms, and its own msgs:{topic}:{dir} and bytes:{topic}:{dir}. path and path_ms:{path} carry the path this entry last reported in its stats, unknown until it reports one.
transportsarray of TransportTrafficThe traffic the host reported on each transport, one entry per transport name, server first and then by name. These are the host's own figures, and every entry is a log. Only server, the traffic through the engine, is metered, and its meter is the engine's engine:{measure}:{dir} fields in session, never this entry; peer and a declared transport are logged, never metered. A session whose reports carried only server traffic has the one entry, and one with no reported traffic has none.
controlarray of ControlCountThe control frames the session's clients sent the host, one entry per kind, by kind: presence changes, hellos, pings, heartbeats, transport setup and the rest. The host's log, never a meter: never billed, and no limit reads it. The control plane's meter is the engine's own count, engine:ctl_msgs:{dir} and engine:ctl_bytes:{dir} in session.

Example response

JSON
{
  "sessionId": "s_O2SX-CMRO6O-R6XoN4ywTv",
  "readAtMs": "1774000300000",
  "session": {
    "epoch": "1",
    "seq": "42",
    "host_state": "attached",
    "host_since_ms": "1774000000000",
    "attached_ms": "300000",
    "idle_ms": "0",
    "host_attaches": "1",
    "cur:controller": "1",
    "cur:viewer": "2",
    "pm:controller:connected": "280000",
    "pg:send_input:connected": "280000",
    "bytes:in:to_host:unknown": "512000"
  },
  "participants": [
    {
      "participantId": "h_Y9WDXoY5_Gq2Z2HnlYy8hy",
      "counters": {
        "bundle": "host",
        "state": "connected",
        "grants": "receive_input,broadcast,manage",
        "since_ms": "1774000000000",
        "joined_ms": "1774000000000",
        "ms:connected": "300000",
        "ms_bundle:host": "300000",
        "bytes:out:to_client": "480000"
      }
    },
    {
      "participantId": "p_MWnCKAv7YT4UCIX8ncPLUA",
      "counters": {
        "bundle": "controller",
        "state": "connected",
        "grants": "send_input,receive_broadcast",
        "since_ms": "1774000020000",
        "joined_ms": "1774000020000",
        "path": "turn",
        "path_since_ms": "1774000020000",
        "path_ms:turn": "280000",
        "ms:connected": "280000",
        "ms_bundle:controller": "280000",
        "bytes:in:to_host": "240000"
      }
    }
  ]
}

POST /v1/sessions/{sessionId}/clients/{clientId}/boot

Remove one client from the session and free its place.

Who may call it: A host token, a resume token whose client holds manage, a secret key, organization key or token carrying sessions:manage, or a console sign-in.

Permission: sessions:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
reasonstringnoWhy this client is being booted. It reaches the client as the reason its session ended. One of BOOT_REASON_BOOTED, BOOT_REASON_IDLE, BOOT_REASON_POLICY, BOOT_REASON_KEY_UNVERIFIED, BOOT_REASON_RATE.

Response body

The ParticipantView object, written out under Objects.

Example request

JSON
{
  "reason": "BOOT_REASON_BOOTED"
}

Example response

JSON
{
  "participantId": "p_MWnCKAv7YT4UCIX8ncPLUA",
  "seat": 1,
  "seatClass": "controller",
  "state": "gone"
}

PATCH /v1/sessions/{sessionId}/clients/{clientId}

Replace what one client is allowed to do. Every client in the session is told.

Who may call it: A host token, a resume token whose client holds manage, a secret key, organization key or token carrying sessions:manage, or a console sign-in.

Permission: sessions:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
grantsarray of stringyesReplaces the full set; empty is refused. One of GRANT_SEND_INPUT, GRANT_RECEIVE_INPUT, GRANT_BROADCAST, GRANT_RECEIVE_BROADCAST, GRANT_MANAGE.

Response body

The ParticipantView object, written out under Objects.

Example request

JSON
{
  "grants": [
    "GRANT_SEND_INPUT",
    "GRANT_RECEIVE_BROADCAST",
    "GRANT_MANAGE"
  ]
}

Example response

JSON
{
  "participantId": "p_MWnCKAv7YT4UCIX8ncPLUA",
  "seat": 1,
  "seatClass": "controller",
  "state": "connected",
  "grants": [
    "GRANT_SEND_INPUT",
    "GRANT_RECEIVE_BROADCAST",
    "GRANT_MANAGE"
  ]
}

Tokens

Narrow a key to one subject for a while, and end one early.

POST /v1/tokens

Mint a token: your key, narrowed to one subject and to a while. The token string is returned once, here, and never again.

Who may call it: A secret key or organization key carrying tokens:mint.

Permission: tokens:mint, on a credential that carries permissions at all.

Request body

FieldTypeRequiredDescription
subjectstringyesThe developer's own user or device id, 1 to 128 characters.
limitsTokenLimitsnoOptional; the limits this token's presentations are held to.
ttlstringnoHow long the token is good for, as a duration string: 10m, 1h, 720h. Leave it out for the default, 10m. The ceiling is the project's tokensMaxTtl, and a token carrying sessions:join is held to 1h whatever the project allows; above the ceiling that applies is refused validation with details.field ttl.
sdkstringnoSame value as X-Bitskiff-SDK.
permissionsarray of stringnoWhat this token may do, from the session words and no others: sessions:open, sessions:read, sessions:manage, sessions:join. Leave it out and the token carries sessions:open, which is the default. Any other word is refused validation with details.field permissions.
sessionIdstringnoOptional, sessions:join only: this session and no other.
cnfTokenConfirmationnoOptional, sessions:join only.
grantsCeilingarray of stringnoOptional, sessions:join only: the most the client this token admits may ever hold in the session. One of GRANT_SEND_INPUT, GRANT_RECEIVE_INPUT, GRANT_BROADCAST, GRANT_RECEIVE_BROADCAST, GRANT_MANAGE.

Response body

FieldTypeDescription
tokenIdstringtok_..., the id GET and DELETE address it by.
tokenstringbsk_at_..., returned once and stored only as its SHA-256.
expiresAtstring (64-bit integer)Unix ms.

Example request

JSON
{
  "subject": "user_8123",
  "permissions": [
    "sessions:open"
  ],
  "ttl": "1h"
}

Example response

JSON
{
  "tokenId": "tok_XFWqg3VKViwNuxz2",
  "token": "bsk_at_...",
  "expiresAt": "1774003600000"
}

GET /v1/tokens

List the tokens the project holds, by id and paged. No token string is ever returned.

Who may call it: A secret key or organization key carrying tokens:mint.

Permission: tokens:mint, on a credential that carries permissions at all.

Query parameters

These travel in the query string. This route takes no body.

ParameterTypeRequiredDescription
subjectstringnoKeep only the tokens minted for this subject. Omit it for every subject.
keyIdstringnoKeep only the tokens minted by this key. Omit it for every key.
cursorstringnoWhere to carry on from: the nextCursor the page before this one answered with. It is opaque, and it belongs to this listing and this project: one this service did not answer with, or one from another listing, is refused validation with details.field cursor rather than answering an empty page.
limitnumbernoHow many tokens this page may answer with, 1 to 200. Omit it for 50. Every paged route takes the same bound, and a value outside it is refused validation with details.field limit rather than shrunk.

Response body

FieldTypeDescription
tokensarray of TokenSummaryOne live token per row, and never a token string.
nextCursorstringWhere the next page starts. Send it back as cursor. It is absent when this page is the last, so read the pages until it stops coming rather than until it is empty.

Example response

JSON
{
  "tokens": [
    {
      "tokenId": "tok_XFWqg3VKViwNuxz2",
      "subject": "user_8123",
      "keyId": "key_ds7tq99qnn414r8g",
      "permissions": [
        "sessions:open"
      ],
      "mintedAt": "1774000000000",
      "expiresAt": "1774003600000"
    }
  ]
}

DELETE /v1/tokens/{tokenId}

End one token. Sessions it opened keep running; close those yourself.

Who may call it: A secret key or organization key carrying tokens:mint.

Permission: tokens:mint, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

None. The service answers 204.

Join

What a client calls with a join code, what a registered app calls with a handoff ticket, and what the handoff host page reads by the ticket's hash. These are the only routes a client reaches.

POST /v1/join/inspect

What a client can show before it joins: whether the code is good, and how many clients can still join. It consumes nothing.

Who may call it: A publishable key.

Permission: sessions:join, on a credential that carries permissions at all.

Request body

FieldTypeRequiredDescription
codestringyesThe join code, as the client read it out of its link.
sdkstringnoWhich SDK is calling and its version, such as ts-client/0.1.0.
originstringnoWhere this client says it is, declared once on the BitskiffClient: a URL or a custom scheme.
reserveJoinReservenoGated joins: present to turn this fresh code into a join grant as well, so an entry survives a sign-in slower than the code's window.

Response body

FieldTypeDescription
sessionIdstringThe session the code belongs to.
statestringA joinable session state: "open" | "host_attached" | "host_away".
clientsFreenumberHow many more clients this session will take, counted against its maxClients. A join page that is full can say so before it tries to join.
regionstringThe region the session runs in.
protocolnumberThe session's pinned version.
maxClientsnumberThe total clientsFree is counted against: SessionConfig.maxClients as the session answered it.
e2eebooleanEnd-to-end encryption: the session's own mode, fixed at open.
requireTokenstringWhether a join into this session needs a token: off, token, or token_bound_to_session. A join page reads it first, so a client without a token is told one is needed rather than refused token_required.
grantJoinGrantThe join grant a request carrying reserve asked for; absent when none was asked for, and when the reservation was refused.
reservationRefusedReservationRefusedWhy a reservation was refused, when it was.

Example request

JSON
{
  "code": "1CAbCdEfG1HPWtg",
  "sdk": "ts-client/0.2.0"
}

Example response

JSON
{
  "sessionId": "s_O2SX-CMRO6O-R6XoN4ywTv",
  "state": "open",
  "clientsFree": 1,
  "region": "us-central1",
  "protocol": 1,
  "maxClients": 2,
  "requireToken": "off"
}

POST /v1/join

Take a join code, or the join grant inspect reserved from one, and answer the ticket a client connects on, with the resume token it comes back with.

Who may call it: A publishable key.

Permission: sessions:join, on a credential that carries permissions at all.

Request body

FieldTypeRequiredDescription
codestringnoThe join code, as the client read it out of its link.
grantJoinGrantEntrynoThe join grant POST /v1/join/inspect reserved from a fresh code, and its holder secret.
protocolProtocolRangenoThe protocol versions the calling SDK can speak. A join is refused protocol_unsupported when the session's pinned version is outside this range.
sdkstringnoWhich SDK is calling and its version, such as ts-client/0.1.0.
priorClientIdstringnoBest-effort continuity hint; not an identity claim.
e2eebooleannoThe client's capability, never the fingerprint: on a session whose e2ee is true, a join sent with this false—or from an sdk too old to carry the schedule—is refused e2ee_required as a courtesy before any capacity is reserved.
originstringnoWhere this client says it is: a URL or a custom scheme. The service reads this first and falls back to the Origin header only when it is empty, because a page inside an app shell sends capacitor://localhost, http://localhost or no origin at all. POST /v1/join/inspect takes the same field, and this is where a publishable key's origin list is enforced.
environmentClientEnvironmentnoThe program this client runs in, stated by its SDK (ClientEnvironment).
metadataobject, string keys to stringnoThe developer's own metadata for this client (join(code, { metadata })), inside the client metadata bounds: over them is refused validation naming metadata or metadata.<key>, before any capacity is reserved.

Response body

FieldTypeDescription
ticketTicketWhat this client connects its transport with.
resumeTokenstringWhat this client presents to take its place back after a drop. Every resume rotates it.
reconnectBudgetMsnumberHow long this client may spend reconnecting before its place is given up.
backgroundBudgetMsnumberHow long this client may spend backgrounded before its place is given up.
seatDeadlineMsstring (64-bit integer)Absolute Unix ms on the service's clock: the instant past which this resume token is refused and the client's place is gone.
serverTimeMsstring (64-bit integer)The service's own Unix ms at the moment it answered.
limitsSessionLimitsThe caps this client is held to; a client holds no project key and so never calls the limits route.
policyDeadlineMsstring (64-bit integer)Absolute Unix ms on the service's clock this client's soonest termination-policy deadline is due at, 0 when no rule is armed against it: the instant ParticipantTransition.policyDeadlineMs` tells the host, told to the client it concerns. |
registeredAppsarray of RegisteredAppThe native apps the project registered, every platform's: the hosted join page reads the one for this client's platform off this answer and nowhere else, and shows the app offer only where one exists.
handoffHintHandoffHintOn the answer to POST /v1/handoff only: the page's transport as it stated it on the mint, absent when it stated none.
handoffDataTextstringThe handoff data, passed as a string.
handoffDataBytesstring (base64)The handoff data, passed as bytes.
e2eebooleanOn the answer to POST /v1/handoff only: whether the place runs end-to-end encryption, the session's own fixed value (SessionView.e2ee).
hostIdentityPublicstring (base64)On the answer to POST /v1/handoff only, for a place that encrypts: the host identity key the page stated on the mint (HandoffTicketRequest.hostIdentityPublic).

Example request

JSON
{
  "code": "1CAbCdEfG1HPWtg",
  "protocol": {
    "min": 1,
    "max": 1
  },
  "sdk": "ts-client/0.2.0"
}

Example response

JSON
{
  "ticket": {
    "sessionId": "s_O2SX-CMRO6O-R6XoN4ywTv",
    "participantId": "p_MWnCKAv7YT4UCIX8ncPLUA",
    "grants": [
      "GRANT_SEND_INPUT",
      "GRANT_RECEIVE_BROADCAST"
    ],
    "expiresAt": "1774000060000"
  },
  "resumeToken": "bsk_rt_...",
  "reconnectBudgetMs": 10000,
  "backgroundBudgetMs": 30000,
  "serverTimeMs": "1774000000000"
}

POST /v1/handoff

The registered app redeems a handoff ticket and is answered as the same client; the place becomes the app's when its connection attaches.

Who may call it: A publishable key.

Permission: sessions:join, on a credential that carries permissions at all.

Request body

FieldTypeRequiredDescription
ticketstringyesThe handoff ticket the page put in the launch link's fragment, read by the app and sent once.
protocolProtocolRangenoAs JoinRequest.protocol: the protocol range this SDK speaks.
sdkstringnoAs JoinRequest.sdk: the SDK's name and version, for the inventory.
originstringnoAs JoinRequest.origin: the app's declared origin, read first, the Origin header when empty.
e2eebooleannoAs JoinRequest.e2ee: the app's capability, never the fingerprint.
keyThumbprintstringnoThe RFC 7638 SHA-256 thumbprint of the X25519 key the app's Hello will present, the value client.keyThumbprint() gives.
environmentClientEnvironmentnoAs JoinRequest.environment: the app's own.
metadataobject, string keys to stringnoAs JoinRequest.metadata: the app's metadata for the place, the place's at the commit when it is not empty; an empty map keeps the page's.

Response body

FieldTypeDescription
ticketTicketWhat this client connects its transport with.
resumeTokenstringWhat this client presents to take its place back after a drop. Every resume rotates it.
reconnectBudgetMsnumberHow long this client may spend reconnecting before its place is given up.
backgroundBudgetMsnumberHow long this client may spend backgrounded before its place is given up.
seatDeadlineMsstring (64-bit integer)Absolute Unix ms on the service's clock: the instant past which this resume token is refused and the client's place is gone.
serverTimeMsstring (64-bit integer)The service's own Unix ms at the moment it answered.
limitsSessionLimitsThe caps this client is held to; a client holds no project key and so never calls the limits route.
policyDeadlineMsstring (64-bit integer)Absolute Unix ms on the service's clock this client's soonest termination-policy deadline is due at, 0 when no rule is armed against it: the instant ParticipantTransition.policyDeadlineMs` tells the host, told to the client it concerns. |
registeredAppsarray of RegisteredAppThe native apps the project registered, every platform's: the hosted join page reads the one for this client's platform off this answer and nowhere else, and shows the app offer only where one exists.
handoffHintHandoffHintOn the answer to POST /v1/handoff only: the page's transport as it stated it on the mint, absent when it stated none.
handoffDataTextstringThe handoff data, passed as a string.
handoffDataBytesstring (base64)The handoff data, passed as bytes.
e2eebooleanOn the answer to POST /v1/handoff only: whether the place runs end-to-end encryption, the session's own fixed value (SessionView.e2ee).
hostIdentityPublicstring (base64)On the answer to POST /v1/handoff only, for a place that encrypts: the host identity key the page stated on the mint (HandoffTicketRequest.hostIdentityPublic).

Example request

JSON
{
  "ticket": "bsk_hf_...",
  "protocol": {
    "min": 1,
    "max": 1
  },
  "sdk": "android-client/0.2.0"
}

Example response

JSON
{
  "ticket": {
    "sessionId": "s_O2SX-CMRO6O-R6XoN4ywTv",
    "participantId": "p_MWnCKAv7YT4UCIX8ncPLUA",
    "grants": [
      "GRANT_SEND_INPUT",
      "GRANT_RECEIVE_BROADCAST"
    ],
    "expiresAt": "1774000420000"
  },
  "resumeToken": "bsk_rt_...",
  "reconnectBudgetMs": 10000,
  "backgroundBudgetMs": 30000,
  "serverTimeMs": "1774000360000",
  "handoffHint": {
    "transport": "peer",
    "grade": "good",
    "rttMs": 12
  },
  "handoffDataText": "player-7"
}

GET /v1/handoff/{ticketHash}/apps

The handoff host page reads the registered apps of the project a handoff ticket was minted for, by the ticket's hash, so it offers that project's app and no other.

Who may call it: Anyone, with no credential.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
registeredAppsarray of RegisteredAppThe project's registered apps, one per platform, with clientKeyId and fingerprints empty.

Example response

JSON
{
  "registeredApps": [
    {
      "platform": "android",
      "appId": "com.example.race",
      "handoffLink": "https://links.example.com/race/",
      "name": "Race",
      "gain": "Steer over Bluetooth"
    }
  ]
}

Project keys

Create, change, rotate and revoke the keys a project signs in with.

GET /v1/project/keys

List the project's keys. A secret key's string is truncated; a publishable key's is not.

Who may call it: An organization key carrying keys:manage, or a console sign-in.

Permission: keys:manage, on a credential that carries permissions at all.

Request body

None.

Response body

FieldTypeDescription
keysarray of KeyViewOne key per row, each carrying masked rather than the key itself. A secret key is truncated there; a publishable key is stored and listed in clear, so its masked is the whole key.

Example response

JSON
{
  "keys": [
    {
      "keyId": "key_ds7tq99qnn414r8g",
      "type": "publishable",
      "projectId": "proj_g1qr384an9asb4kdp10e",
      "masked": "bsk_pk_hp5ymbh2...n3x1",
      "status": "active",
      "label": "web host",
      "origins": [
        "https://example.com"
      ],
      "createdAt": "1769990400000",
      "expiresAt": "1801526400000"
    }
  ]
}

POST /v1/project/keys

Create a publishable or secret key. A secret key must name its permissions; a publishable one may not. The full key string is returned once, here, and never again.

Who may call it: An organization key carrying keys:manage, or a console sign-in.

Permission: keys:manage, on a credential that carries permissions at all.

Request body

FieldTypeRequiredDescription
typestringyes"publishable" | "secret".
labelstringnoYour own name for the key. It is what the console and the audit rows show.
originsarray of stringnoThe origins a publishable key may be used from. A page on any other origin is refused origin_not_allowed.
limitsKeyLimitsnoCaps this key is held to, each at or below the project's own.
expiresAtstring (64-bit integer)noWhen the key expires, Unix ms; 0 takes the default.
permissionsarray of stringnoWhat a secret key may do, one name per route family, such as sessions:open. Required on a secret key and refused on a publishable one. Name this or preset, never both: sending both is refused validation with details.field permissions. Credentials lists every name.
presetstringnoOne of the console's ready-made permission lists instead of your own: "full-access" | "host-only" | "monitor". Name this or permissions, never both.
hostKeysarray of stringnoA client key only: the ids of the host keys of the same project whose sessions it may join.
subjectsarray of stringnoA secret key holding tokens:mint only: the subject prefixes a mint under it may name, as display:.

Response body

The KeyView object, written out under Objects.

Example request

JSON
{
  "type": "secret",
  "label": "backend",
  "permissions": [
    "sessions:open",
    "tokens:mint"
  ]
}

Example response

JSON
{
  "keyId": "key_hn76yrbsbdghwpdv",
  "type": "secret",
  "projectId": "proj_g1qr384an9asb4kdp10e",
  "secret": "bsk_sk_...",
  "masked": "bsk_sk_tthy8dbr...nbn7",
  "status": "active",
  "label": "backend",
  "permissions": [
    "sessions:open",
    "tokens:mint"
  ],
  "createdAt": "1774000000000",
  "expiresAt": "1801526400000"
}

PATCH /v1/project/keys/{keyId}

Change a key's label, origins, permissions, limits or expiry. Send only what you are changing.

Who may call it: An organization key carrying keys:manage, or a console sign-in.

Permission: keys:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
retireAtstring (64-bit integer)noMove when a rotating key stops being accepted, Unix ms. Omit it to leave it alone.
labelstringnoA new name for the key. Omit it to leave the name alone.
originsStringListPatchnoReplace the origin list. Omit it to leave the list alone; send it with no values to clear it.
limitsKeyLimitsnoNew caps for the key. Each field applies on its own, and one left out is left alone.
expiresAtstring (64-bit integer)noBring the key's expiry forward, Unix ms. It may only be lowered, and never past the expiry of the key making the change. Omit it to leave the expiry alone.
permissionsStringListPatchnoSecret keys only; validates exactly as POST /project/keys does.
hostKeysStringListPatchnoA client key only, validated as the create does.
presetstringnoA secret key's preset instead of permissions ("full-access", "host-only", "monitor"), expanded before the write; naming both is a validation refusal.
subjectsStringListPatchnoA secret key holding tokens:mint only, validated as the create does.

Response body

The KeyView object, written out under Objects.

Example request

JSON
{
  "label": "backend (eu)",
  "origins": {
    "values": [
      "https://example.com"
    ]
  }
}

Example response

JSON
{
  "keyId": "key_hn76yrbsbdghwpdv",
  "type": "secret",
  "projectId": "proj_g1qr384an9asb4kdp10e",
  "masked": "bsk_sk_tthy8dbr...nbn7",
  "status": "active",
  "label": "backend (eu)"
}

DELETE /v1/project/keys/{keyId}

Revoke a key. reason=compromise ends every session it opened at once; reason=manual lets live work drain and only stops the key authorizing anything new. Either way the revocation follows through to every key this one created.

Who may call it: An organization key carrying keys:manage, or a console sign-in.

Permission: keys:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Query parameters

These travel in the query string. This route takes no body.

ParameterTypeRequiredDescription
reasonstringyes"manual" | "compromise". There is no default: leaving it out, or sending anything else, is refused validation with details.field reason.

Response body

FieldTypeDescription
keyKeyViewThe key as it now stands, revoked.
sessionsEndedCountstring (64-bit integer)How many sessions this key had open were ended with it.

Example response

JSON
{
  "key": {
    "keyId": "key_hn76yrbsbdghwpdv",
    "type": "secret",
    "status": "revoked",
    "revokedAt": "1774000500000",
    "revokedReason": "compromise"
  },
  "sessionsEndedCount": "3"
}

POST /v1/project/keys/{keyId}/rotate

Start a rotation. Answers the new key, and the old one keeps working until the overlap runs out.

Who may call it: An organization key carrying keys:manage, or a console sign-in.

Permission: keys:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
overlapSnumbernoHow many seconds the old key keeps being accepted for. Omit it to take the platform's own overlap.

Response body

The KeyView object, written out under Objects.

Example request

JSON
{
  "overlapS": 86400
}

Example response

JSON
{
  "keyId": "key_8cqgjhdz4hyy0dfe",
  "type": "secret",
  "projectId": "proj_g1qr384an9asb4kdp10e",
  "secret": "bsk_sk_...",
  "masked": "bsk_sk_tthy8dbr...p878",
  "status": "active",
  "label": "backend",
  "createdAt": "1774000600000",
  "expiresAt": "1801526400000"
}

Fixed codes

Make, list, relabel, replace and delete the join codes that do not rotate, for an NFC tag or a printed QR. A host names one by its id when it opens.

GET /v1/project/fixed-codes

List the project's fixed codes: label, id and the session holding each one now. Never the code itself.

Who may call it: An organization key carrying project:read, or a console sign-in.

Permission: project:read, on a credential that carries permissions at all.

Request body

None.

Response body

FieldTypeDescription
fixedCodesarray of FixedCodeViewThis page's fixed codes, oldest first.
nextCursorstringEmpty when this page is the last.

Example response

JSON
{
  "fixedCodes": [
    {
      "id": "fc_3Qb8KfR2m0VxL7nT9pZc1w",
      "label": "front door",
      "sessionId": "s_O2SX-CMRO6O-R6XoN4ywTv",
      "createdAtMs": "1769990400000"
    }
  ]
}

POST /v1/project/fixed-codes

Make a fixed code for an NFC tag or a printed QR. This answer is one of the two places the code is ever shown.

Who may call it: An organization key carrying project:manage, or a console sign-in.

Permission: project:manage, on a credential that carries permissions at all.

Request body

FieldTypeRequiredDescription
labelstringnoAt most 64 UTF-8 bytes; nothing resolves by it.

Response body

The FixedCodeView object, written out under Objects.

Example request

JSON
{
  "label": "front door"
}

Example response

JSON
{
  "id": "fc_3Qb8KfR2m0VxL7nT9pZc1w",
  "label": "front door",
  "code": "FCq8Rz2LmWp4Xk7",
  "createdAtMs": "1769990400000"
}

PATCH /v1/project/fixed-codes/{fixedCodeId}

Change a fixed code's label.

Who may call it: An organization key carrying project:manage, or a console sign-in.

Permission: project:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
labelstringnoThe new label, at most 64 UTF-8 bytes.

Response body

The FixedCodeView object, written out under Objects.

Example request

JSON
{
  "label": "bar, left of the door"
}

Example response

JSON
{
  "id": "fc_3Qb8KfR2m0VxL7nT9pZc1w",
  "label": "bar, left of the door",
  "createdAtMs": "1769990400000"
}

POST /v1/project/fixed-codes/{fixedCodeId}/replace

Replace a fixed code: every copy of the old code stops working at once, and the id stays.

Who may call it: An organization key carrying project:manage, or a console sign-in.

Permission: project:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

The FixedCodeView object, written out under Objects.

Example response

JSON
{
  "id": "fc_3Qb8KfR2m0VxL7nT9pZc1w",
  "label": "front door",
  "code": "FCt5Ny9HdQ3sJw6",
  "createdAtMs": "1769990400000",
  "replacedAtMs": "1770076800000"
}

DELETE /v1/project/fixed-codes/{fixedCodeId}

Delete a fixed code. Its code stops working; a session holding it keeps its clients.

Who may call it: An organization key carrying project:manage, or a console sign-in.

Permission: project:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

None. The service answers 204.

Project limits and settings

What the project allows, what it can open now, what it is set to, the raises waiting on an owner, and renaming or archiving it.

GET /v1/project/limits

Read the project's limits: the ceilings, the concurrency gates and what is in use right now.

Who may call it: A secret key or organization key carrying project:read, or a console sign-in.

Permission: project:read, on a credential that carries permissions at all.

Request body

None.

Response body

FieldTypeDescription
ceilingsSessionConfigThe highest each session-config field may be opened with on this project. A duration of 0s here is no ceiling at all, not a ceiling of zero: the field is unbounded, and a session may be opened with any value the grammar accepts. A request above a ceiling is clamped down to it rather than refused, and the session tells you what it was clamped to.
maxConcurrentSessionsnumberThe most sessions this project may hold open at once.
maxConcurrentClientsnumberThe most clients this project may hold at once. It is the number a join meets before a session's own maxClients.
concurrentSessionsnumberHow many sessions this project has open right now, counted against maxConcurrentSessions.
concurrentClientsnumberHow many clients this project holds right now, counted against maxConcurrentClients. A host does not count.
sessionOpensPerMinutenumberHow many sessions this project may open in a minute.
joinsPerMinuteSessionnumberThe joinsPerMinuteSession row, spelled here the way details.limit, LimitsPatch and bitskiff limits set spell it.
bytesPerSecondnumberThe one per-client byte cap: the bytesPerSecond row, per client per direction.
clientMinutesPerDayLimitQuotaDay counter, resets 00:00 UTC.
monthlySpendCapLimitQuotaThe cap on what this project may spend in a month, and what is left of it. Spend is counted in rated units, one per client-minute, and the count rolls over with the month.
turnBytesPerMonthLimitQuotaBytes, month rollover.
ticketMintsPerMinuteLimitQuotaKey-scoped rate limit; used and periodEnd are 0.
clientResumesPerMinutenumberHow many times one client may resume in a minute.
statsReportsPerMinuteSessionnumberHow many stats reports one session may post in a minute.
tokensMaxTtlstringThe longest a join token minted for this project may live, as a duration string—1h unless the project has raised it, up to 720h. It bounds the ttl a mint asks for and a live token's expiry, so lowering it shortens tokens already out. A key may hold itself to less, which is that key's own quota rather than this value.
joinsPerMinuteIpnumberHow many joins this project admits from one address in a minute—20 unless the project has raised it, up to 200.
joinsPerMinuteKeynumberThe joinsPerMinuteKey row: joins one client key admits in a minute, across every session and address it joins from.
messagesPerSecondnumberThe messagesPerSecond row's bitskiff.in rate: the project's own value, 120 unless the project has lowered it.
linkMessagesPerSecondnumberThe linkMessagesPerSecond row: the link ceiling, the project's own value, equal to messagesPerSecond unless the project's plan raised it; a client key's own limits.linkMessagesPerSecond may sit under it.
bootCooldownstringbootCooldown: how long a subject the host core booted with rate is refused every join into the project, as the duration string a person types (10m unless the project has lowered it, 0s for none).
requireTokenstringWhether a join into this project needs a token: off, token, or token_bound_to_session—off unless the project has set it. A key may require more for its own sessions, never less.
samplePageSessionsSamplePageSessionsHow many sessions this project has opened on the platform sample page, and the two bounds that count meets. Absent where the environment has no sample page.

Example response

JSON
{
  "maxConcurrentSessions": 5,
  "maxConcurrentClients": 40,
  "concurrentSessions": 3,
  "concurrentClients": 7,
  "sessionOpensPerMinute": 10,
  "joinsPerMinuteSession": 30,
  "bytesPerSecond": 8192,
  "clientMinutesPerDay": {
    "value": "2000",
    "scope": "project",
    "used": "740",
    "periodEnd": "1774051200000"
  },
  "requireToken": "off"
}

GET /v1/project

Read the project the header names: its name, its slug, when it was created and its settings.

Who may call it: An organization key carrying project:read, or a console sign-in.

Permission: project:read, on a credential that carries permissions at all.

Request body

None.

Response body

The ProjectView object, written out under Objects.

Example response

JSON
{
  "projectId": "proj_g1qr384an9asb4kdp10e",
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "name": "Acme demo",
  "slug": "acme-demo",
  "createdAt": "1769990400000",
  "joinUrl": "https://example.com/join",
  "e2ee": "off"
}

GET /v1/project/settings

Read the project's settings: where a join link points, and whether end-to-end encryption is required.

Who may call it: An organization key carrying project:read, or a console sign-in.

Permission: project:read, on a credential that carries permissions at all.

Request body

None.

Response body

FieldTypeDescription
joinUrlstringThe page a join code points a client at.
e2eestring"off" | "required".
warningsarray of stringNon-fatal notes, such as a joinUrl no key's allowlist covers.
restrictedModebooleanPublishable keys cannot open new sessions while this is on.
terminationRulesarray of TerminationRuleThe project's default termination policy: the rule list a session takes when its open names none.
sessionDefaultsSessionDefaultsThe project's session defaults: the value each session-config field takes when an open leaves it out.
registeredAppsarray of RegisteredAppThe native apps the project registered, one per platform at most.

Example response

JSON
{
  "joinUrl": "https://example.com/join",
  "e2ee": "off"
}

PATCH /v1/project/settings

Change the project's settings. Send only what you are changing; a join URL set to an empty string clears it.

Who may call it: An organization key carrying project:manage, or a console sign-in.

Permission: project:manage, on a credential that carries permissions at all.

Request body

FieldTypeRequiredDescription
joinUrlstringnoA new join URL. Omit it to leave the setting alone.
e2eestringno"off" | "required".
restrictedModebooleannoReversible, and nothing live ends.
terminationRulesTerminationRuleListPatchnoThe project's default termination rule list, replaced whole.
sessionDefaultsSessionDefaultsnoThe project's session defaults, field by field: a field set here becomes that field's default and a field left empty (or 0 for maxClients) keeps the one it has.
registeredAppsRegisteredAppListPatchnoThe registered apps, replaced whole.

Response body

FieldTypeDescription
joinUrlstringThe page a join code points a client at.
e2eestring"off" | "required".
warningsarray of stringNon-fatal notes, such as a joinUrl no key's allowlist covers.
restrictedModebooleanPublishable keys cannot open new sessions while this is on.
terminationRulesarray of TerminationRuleThe project's default termination policy: the rule list a session takes when its open names none.
sessionDefaultsSessionDefaultsThe project's session defaults: the value each session-config field takes when an open leaves it out.
registeredAppsarray of RegisteredAppThe native apps the project registered, one per platform at most.

Example request

JSON
{
  "joinUrl": "https://example.com/join"
}

Example response

JSON
{
  "joinUrl": "https://example.com/join",
  "e2ee": "off"
}

GET /v1/project/capacity

Read what the project can open now: its capacity standing, and how many more sessions of the shape you name, and the clients they hold, would be placed. A reading, not a reservation: every open still judges every limit.

Who may call it: A secret key or organization key carrying project:read, or a console sign-in.

Permission: project:read, on a credential that carries permissions at all.

Query parameters

These travel in the query string, not in a body.

FieldTypeRequiredDescription
maxClientsnumbernoThe clients each session holds; 0 reads the project's maxClients ceiling.
lossyHznumbernoThe summed maxHz of the lossy rows each session declares; 0 is none.
reliableHznumbernoThe summed maxHz of the reliable rows each session declares; 0 is none.
maxBytesnumbernoA message's ceiling on those rows; 0 is the wire ceiling, as an open row typing none.

Response body

FieldTypeDescription
standingCapacityNoticeAlways present; OK over an empty list when nothing stands.
sessionsnumberHow many more sessions of the asked shape would be placed now, one after another: the smaller of what the concurrency gates leave and what the service has room for.
clientsnumberThe clients those sessions hold, held to what concurrentClients leaves.
maxClientsnumberThe maxClients the reading priced each session at.

Example query

HTTP
GET /v1/project/capacity?maxClients=4&lossyHz=30

Example response

JSON
{
  "standing": {
    "status": "CAPACITY_STATUS_WARNING",
    "limits": [
      {
        "limit": "clientMinutesPerDay",
        "scope": "project",
        "scopeId": "proj_7f3k2m9xqw4tz8bn",
        "status": "CAPACITY_STATUS_WARNING",
        "value": "1640",
        "ceiling": "2000",
        "resetAt": "1774051200000"
      }
    ],
    "at": "1773990000000"
  },
  "sessions": 2,
  "clients": 8,
  "maxClients": 4
}

GET /v1/projects/{projectId}

Read one project by its slug or id, archived or not.

Who may call it: An organization key carrying project:read, or a console sign-in.

Permission: project:read, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

The ProjectView object, written out under Objects.

Example response

JSON
{
  "projectId": "proj_g1qr384an9asb4kdp10e",
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "name": "Acme demo",
  "slug": "acme-demo",
  "createdAt": "1769990400000",
  "joinUrl": "https://example.com/join",
  "e2ee": "off"
}

PATCH /v1/projects/{projectId}

Rename a project. The slug never changes.

Who may call it: An organization key carrying project:manage, or a console sign-in.

Permission: project:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
namestringyesThe project's new name, 1 to 64 characters. The slug stays as it is.

Response body

The ProjectView object, written out under Objects.

Example request

JSON
{
  "name": "Acme lobby"
}

Example response

JSON
{
  "projectId": "proj_g1qr384an9asb4kdp10e",
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "name": "Acme lobby",
  "slug": "acme-demo",
  "createdAt": "1769990400000",
  "joinUrl": "https://example.com/join",
  "e2ee": "off"
}

POST /v1/projects/{projectId}/archive

Archive a project. Nothing is deleted: it refuses new sessions, keys and tokens, keeps its ledgers and usage, and an owner can unarchive it for 30 days.

Who may call it: An organization key carrying project:manage, or a console sign-in.

Permission: project:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

The ProjectView object, written out under Objects.

Example response

JSON
{
  "projectId": "proj_g1qr384an9asb4kdp10e",
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "name": "Acme demo",
  "slug": "acme-demo",
  "createdAt": "1769990400000",
  "joinUrl": "https://example.com/join",
  "e2ee": "off",
  "archivedAt": "1774915200000"
}

POST /v1/projects/{projectId}/unarchive

Unarchive a project within 30 days of archiving it, so it takes new sessions, keys and tokens again.

Who may call it: An organization key carrying project:manage, or a console sign-in.

Permission: project:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

The ProjectView object, written out under Objects.

Example response

JSON
{
  "projectId": "proj_g1qr384an9asb4kdp10e",
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "name": "Acme demo",
  "slug": "acme-demo",
  "createdAt": "1769990400000",
  "joinUrl": "https://example.com/join",
  "e2ee": "off"
}

PUT /v1/project/limits

Set the project's limits, within your plan. Lowering one takes effect at once and never ends live work.

Who may call it: An organization key carrying project:manage, or a console sign-in.

Permission: project:manage, on a credential that carries permissions at all.

Request body

FieldTypeRequiredDescription
concurrentSessionsnumbernoA new ceiling on sessions open at once. Omit it to leave the current one. The response calls this ceiling maxConcurrentSessions and keeps concurrentSessions for the live count.
concurrentClientsnumbernoA new ceiling on clients in this project at once. Omit it to leave the current one. The response calls this ceiling maxConcurrentClients and keeps concurrentClients for the live count.
sessionOpensPerMinutenumbernoA new ceiling on session opens per minute. Omit it to leave the current one.
joinsPerMinuteSessionnumbernoA new ceiling on joins per minute for one session. Omit it to leave the current one. The response spells the same limit the same way.
clientResumesPerMinutenumbernoA new ceiling on resumes per minute for one client. Omit it to leave the current one.
statsReportsPerMinuteSessionnumbernoA new ceiling on stats reports per minute for one session. Omit it to leave the current one.
clientMinutesPerDaystring (64-bit integer)noA new cap on client minutes a day. Omit it to leave the current one.
monthlySpendCapstring (64-bit integer)noA new monthly spend cap, in rated units. Omit it to leave the current one.
turnBytesPerMonthstring (64-bit integer)noA new cap on relayed bytes a month. Omit it to leave the current one.
ticketMintsPerMinutenumbernoA new ceiling on tickets minted per minute. Omit it to leave the current one.
joinsPerMinuteKeynumbernoA client key's join window, project-wide.
messagesPerSecondnumbernoThe bitskiff.in rate, lowered only (at most 120).
linkMessagesPerSecondnumbernoThe link ceiling, raised per plan.
bootCooldownstringnoThe bootCooldown row as a duration string (5m, 0s for none), lowered only (at most 10m); a value that is not a duration is validation naming the field.
bytesPerSecondnumbernoThe byte row per client per direction, raised up to the plan's ceiling; a client key's own value sits under it.

Response body

FieldTypeDescription
ceilingsSessionConfigThe highest each session-config field may be opened with on this project. A duration of 0s here is no ceiling at all, not a ceiling of zero: the field is unbounded, and a session may be opened with any value the grammar accepts. A request above a ceiling is clamped down to it rather than refused, and the session tells you what it was clamped to.
maxConcurrentSessionsnumberThe most sessions this project may hold open at once.
maxConcurrentClientsnumberThe most clients this project may hold at once. It is the number a join meets before a session's own maxClients.
concurrentSessionsnumberHow many sessions this project has open right now, counted against maxConcurrentSessions.
concurrentClientsnumberHow many clients this project holds right now, counted against maxConcurrentClients. A host does not count.
sessionOpensPerMinutenumberHow many sessions this project may open in a minute.
joinsPerMinuteSessionnumberThe joinsPerMinuteSession row, spelled here the way details.limit, LimitsPatch and bitskiff limits set spell it.
bytesPerSecondnumberThe one per-client byte cap: the bytesPerSecond row, per client per direction.
clientMinutesPerDayLimitQuotaDay counter, resets 00:00 UTC.
monthlySpendCapLimitQuotaThe cap on what this project may spend in a month, and what is left of it. Spend is counted in rated units, one per client-minute, and the count rolls over with the month.
turnBytesPerMonthLimitQuotaBytes, month rollover.
ticketMintsPerMinuteLimitQuotaKey-scoped rate limit; used and periodEnd are 0.
clientResumesPerMinutenumberHow many times one client may resume in a minute.
statsReportsPerMinuteSessionnumberHow many stats reports one session may post in a minute.
tokensMaxTtlstringThe longest a join token minted for this project may live, as a duration string—1h unless the project has raised it, up to 720h. It bounds the ttl a mint asks for and a live token's expiry, so lowering it shortens tokens already out. A key may hold itself to less, which is that key's own quota rather than this value.
joinsPerMinuteIpnumberHow many joins this project admits from one address in a minute—20 unless the project has raised it, up to 200.
joinsPerMinuteKeynumberThe joinsPerMinuteKey row: joins one client key admits in a minute, across every session and address it joins from.
messagesPerSecondnumberThe messagesPerSecond row's bitskiff.in rate: the project's own value, 120 unless the project has lowered it.
linkMessagesPerSecondnumberThe linkMessagesPerSecond row: the link ceiling, the project's own value, equal to messagesPerSecond unless the project's plan raised it; a client key's own limits.linkMessagesPerSecond may sit under it.
bootCooldownstringbootCooldown: how long a subject the host core booted with rate is refused every join into the project, as the duration string a person types (10m unless the project has lowered it, 0s for none).
requireTokenstringWhether a join into this project needs a token: off, token, or token_bound_to_session—off unless the project has set it. A key may require more for its own sessions, never less.
samplePageSessionsSamplePageSessionsHow many sessions this project has opened on the platform sample page, and the two bounds that count meets. Absent where the environment has no sample page.

Example request

JSON
{
  "concurrentSessions": 25,
  "sessionOpensPerMinute": 60
}

Example response

JSON
{
  "maxConcurrentSessions": 25,
  "maxConcurrentClients": 40,
  "concurrentSessions": 3,
  "concurrentClients": 7,
  "sessionOpensPerMinute": 60,
  "joinsPerMinuteSession": 30,
  "bytesPerSecond": 8192
}

POST /v1/project/limits/raise

Raise or lower one limit. A value within your plan's ceiling applies at once and takes effect on the next open or join; a value above it is held as a pending raise for an organization owner to approve or deny, and the current value applies meanwhile. A raise naming a fixed limit is refused validation. See Raises.

Who may call it: An organization key carrying project:manage, or a console sign-in.

Permission: project:manage, on a credential that carries permissions at all.

Request body

FieldTypeRequiredDescription
limitstringyesWhich limit to raise, by the name a refusal carries in details.limit, such as concurrentSessions.
valuestring (64-bit integer)yesWhat to raise it to.
keyIdstringnoRaise the limit on one key rather than on the project. Omit it for the project.
wordstringnoThe value to set on a row that takes a word rather than a number, such as token for requireToken. Send it in place of value, never beside it.

Response body

FieldTypeDescription
limitstringThe limit this answer is about.
valuestring (64-bit integer)The value that was asked for.
appliedbooleanTrue when that value is in force now. It is true on every raise: no plan carries a ceiling, so nothing is held back.
ceilingstring (64-bit integer)The plan ceiling that held the raise back, where a plan carries one. No plan carries one, so this field is never set.
pendingbooleanTrue where the raise was stored for a console user to resolve with the current value still in force. No plan carries a ceiling, so nothing is stored and this field is never set.
wordstringThe word now in force, when the row takes a word. value is then 0, and the raise is never pending.

Example request

JSON
{
  "limit": "concurrentSessions",
  "value": "50"
}

Example response

JSON
{
  "limit": "concurrentSessions",
  "value": "50",
  "applied": false,
  "ceiling": "5",
  "pending": true
}

GET /v1/orgs/{orgId}/limit-raises

List the pending limit raises across the organization's projects: each project's limit, the value it asked for, the value that applies meanwhile and the plan's ceiling.

Who may call it: An organization key carrying projects:read, or a console sign-in.

Permission: projects:read, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
raisesarray of LimitRaiseViewThe pending raises, by project and then by limit.

Example response

JSON
{
  "raises": [
    {
      "projectId": "proj_g1qr384an9asb4kdp10e",
      "projectName": "Arcade",
      "limit": "concurrentSessions",
      "value": "50",
      "current": "5",
      "ceiling": "5"
    }
  ]
}

POST /v1/orgs/{orgId}/limit-raises/approve

Approve a pending limit raise: the value it asked for applies to the project from the next open or join, if it is within the organization's plan. Organization owners, or a key that manages the project.

Who may call it: An organization key carrying project:manage, or a console sign-in.

Permission: project:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
projectIdstringyesThe project whose raise this resolves, as proj_....
limitstringyesThe limit in its parameter spelling (concurrentSessions), as the list names it.
reasonstringnoWhy, in the owner's words, up to 500 characters: carried on the audit row and on the limits.raise_approved or limits.raise_denied event.

Response body

FieldTypeDescription
projectIdstringThe project whose raise was resolved.
limitstringThe limit in its parameter spelling.
valuestring (64-bit integer)The value the raise asked for, which an approve applied.
approvedbooleantrue for an approve, which applied the value; false for a deny, which left the current value in place.
reasonstringThe reason the owner gave, empty when none was.

Example request

JSON
{
  "projectId": "proj_g1qr384an9asb4kdp10e",
  "limit": "concurrentSessions",
  "reason": "the plan now covers it"
}

Example response

JSON
{
  "projectId": "proj_g1qr384an9asb4kdp10e",
  "limit": "concurrentSessions",
  "value": "50",
  "approved": true,
  "reason": "the plan now covers it"
}

POST /v1/orgs/{orgId}/limit-raises/deny

Deny a pending limit raise, with a reason: the request is cleared and the project's current value stands. Organization owners, or a key that manages the project.

Who may call it: An organization key carrying project:manage, or a console sign-in.

Permission: project:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
projectIdstringyesThe project whose raise this resolves, as proj_....
limitstringyesThe limit in its parameter spelling (concurrentSessions), as the list names it.
reasonstringyesWhy, in the owner's words, up to 500 characters: carried on the audit row and on the limits.raise_approved or limits.raise_denied event.

Response body

FieldTypeDescription
projectIdstringThe project whose raise was resolved.
limitstringThe limit in its parameter spelling.
valuestring (64-bit integer)The value the raise asked for, which an approve applied.
approvedbooleantrue for an approve, which applied the value; false for a deny, which left the current value in place.
reasonstringThe reason the owner gave, empty when none was.

Example request

JSON
{
  "projectId": "proj_g1qr384an9asb4kdp10e",
  "limit": "concurrentSessions",
  "reason": "not this month"
}

Example response

JSON
{
  "projectId": "proj_g1qr384an9asb4kdp10e",
  "limit": "concurrentSessions",
  "value": "50",
  "reason": "not this month"
}

Usage

What the project has used, by day or by month, and the usage records you bill your own customers on.

GET /v1/project/usage

Usage by day or by month over a range, for every meter or for one of them.

Who may call it: A secret key or organization key carrying usage:read, or a console sign-in.

Permission: usage:read, on a credential that carries permissions at all.

Query parameters

These travel in the query string, not in a body.

FieldTypeRequiredDescription
fromstring (64-bit integer)yesMs since epoch, inclusive.
tostring (64-bit integer)yesMs since epoch, exclusive.
granularitystringno"day" | "month"; the two grains the rollups store.
meterstringnoOptional meter id filter; empty means every meter in the bucket.

Response body

FieldTypeDescription
rowsarray of UsageRowOne row per bucket in the window that was asked for.
frozenThroughstring (64-bit integer)The start of the last bucket the rollups have frozen for this grain, ms since epoch, or 0 when they have frozen nothing yet.

Example query

HTTP
GET /v1/project/usage?from=1772323200000&to=1774915200000&granularity=month

Example response

JSON
{
  "rows": [
    {
      "bucketStart": "1772323200000",
      "meters": {
        "sessions.opened": 412,
        "participant_minutes": 4820
      }
    }
  ],
  "frozenThrough": "1774828800000"
}

GET /v1/project/ledger

Usage records over a range, for every subject or for one, as JSON or CSV: one per ended session, one per session per UTC day, or one per ended client. Paged.

Who may call it: A secret key or organization key carrying usage:read, or a console sign-in.

Permission: usage:read, on a credential that carries permissions at all.

Query parameters

These travel in the query string, not in a body.

FieldTypeRequiredDescription
fromstring (64-bit integer)yesThe start of the range, Unix ms, inclusive, on the slice's own instant: endedAt for session, day for day, goneAt for client. Required.
tostring (64-bit integer)yesThe end of the range, Unix ms, exclusive. Required, after from, and at most 31 days after it: a longer range, or one that would read more of the ledger than one read may, is refused validation with details.field to, so read the period in parts.
slicestringnoWhich records: session (the default), one per ended session; day, one per session per UTC day it was alive, which is what a session that outlives an invoice period is billed by; or client, one per ended client. Anything else is refused validation.
subjectstringnoKeep only the records of this subject, exactly: the opener's on a session or day record, the client's own on a client record. Omit it for every record of the project.
cursorstringnoWhere to carry on from: the nextCursor of the page before this one, sent with the same range, slice and subject. It reads the first page's finished result, so it runs no new read, and it lasts 24 hours. One this service did not answer with, one for another filter, or one past its 24 hours is refused validation with details.field cursor.
limitnumbernoHow many records this page may answer with, 1 to 200. Omit it for 50. Every paged route takes the same bound, and a value outside it is refused validation with details.field limit.
formatstringnojson (the default) or csv. CSV answers text/csv: a header row of the record's field names, then one row per record, every cell quoted, and a cell that starts =, +, -, @, a tab or a carriage return prefixed with an apostrophe. The maps are one JSON object per cell, and the next cursor rides the X-Bitskiff-Next-Cursor header, absent on the last page.

Response body

FieldTypeDescription
sessionsarray of SessionUsageThe session or day records of this page, in the slice's instant order and then by session. Answered for slice session and day.
clientsarray of ClientUsageThe client records of this page, by goneAt and then by session and client. Answered for slice client.
nextCursorstringWhere the next page starts. Send it back as cursor with the same range, slice and subject. It is absent when this page is the last.
frozenThroughstring (64-bit integer)Unix ms before which no record of the slice changes any more. Bill a period once this has passed its to; before that the answer is a running figure, and writtenAt on each record says when it last moved.

Example query

HTTP
GET /v1/project/ledger?from=1772323200000&to=1774915200000&slice=session&subject=tenant_42&limit=1

Example response

JSON
{
  "sessions": [
    {
      "id": "k7rw2m5xq3dzt4pnvb6hy2cjle",
      "sessionId": "s_Yk3dQ9wLr2Tn8vBx5Mz1Ca",
      "projectId": "proj_7hq2m9xk4rz3tv8bn6cw",
      "region": "us-central1",
      "keyId": "key_y090cqev9fn21nxp",
      "subject": "tenant_42",
      "label": "lobby-north",
      "metadata": {
        "venue": "north"
      },
      "openedAt": "1772400000000",
      "endedAt": "1772403600000",
      "endedReason": "host_closed",
      "attachedMs": "3540000",
      "detachedMs": "60000",
      "seatMs": "9720000",
      "seatMsByBundle": {
        "controller": "7200000",
        "viewer": "2520000"
      },
      "clients": 4,
      "peakClients": 3,
      "bytesReported": "18432000",
      "dataBytesDown": "15728640",
      "dataBytesUp": "2097152",
      "dataBytesGrade": "server_measured",
      "writtenAt": "1772404200000",
      "writerRevision": 3
    }
  ],
  "nextCursor": "YzEKbGVkZ2VyCnByb2pfN2hxMm05eGs0cnozdHY4Ym42Y3cKeyJqIjoiam9iX2FiIn0",
  "frozenThrough": "1774656000000"
}

Sign-in

How the console signs a person in. These take a console session rather than a key, and no SDK calls them.

POST /v1/auth/refresh

Rotate the console's refresh cookie and answer a fresh console sign-in token.

Who may call it: A console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

None.

Response body

FieldTypeDescription
tokenstringbsk_cs_..., presented as Authorization: Bearer.
expiresAtstring (64-bit integer)Unix ms; identity.consoleTokenTtlS after issue.
userIdstringThe signed-in person's id.
mfaAtstring (64-bit integer)Unix ms this user last passed MFA; 0 when they never have.

Example response

JSON
{
  "token": "bsk_cs_...",
  "expiresAt": "1774000900000",
  "userId": "user_2737116ccba906a9d2229ddfdbf5b812"
}

GET /v1/auth/principal

Who this console sign-in is, and the organizations and projects that console user holds a relation in.

Who may call it: A console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

None.

Response body

FieldTypeDescription
userIdstringThe signed-in person's id.
emailstringAbsent until a console user record carries an address; the console sign-in alone does not.
mfaAtstring (64-bit integer)Unix ms this user last passed MFA; 0 when they never have.
organizationsarray of OrganizationMembershipEvery organization this person holds a relation in, with the highest relation they hold in each.
mfaGraceEndsAtstring (64-bit integer)When this person's second-factor grace ends, in Unix ms: after it, every route but sign-in, enrolment and sign-out refuses them mfa_required. Absent when no organization holds them to a second factor or this session passed one.
projectsarray of ProjectMembershipEvery project this person holds a relation on, directly or through their organization, with each project relation they hold there.

Example response

JSON
{
  "userId": "user_2737116ccba906a9d2229ddfdbf5b812",
  "email": "dev@example.com",
  "organizations": [
    {
      "orgId": "org_2f9wvkpcd87svnzcgkxs",
      "name": "Northwind",
      "relation": "member"
    }
  ],
  "projects": [
    {
      "projectId": "proj_g1qr384an9asb4kdp10e",
      "slug": "acme-demo",
      "orgId": "org_2f9wvkpcd87svnzcgkxs",
      "relations": [
        "key_manager",
        "editor",
        "viewer"
      ]
    }
  ]
}

POST /v1/auth/bootstrap

Claim the first organization on a service that holds none.

Who may call it: A console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

FieldTypeRequiredDescription
orgNamestringnoEmpty takes the default name.
projectNamestringnoEmpty takes the default name.

Response body

FieldTypeDescription
orgIdstringThe organization the claim created.
projectIdstringThe first project in it.

Example request

JSON
{
  "orgName": "Northwind",
  "projectName": "Lobby"
}

Example response

JSON
{
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "projectId": "proj_g1qr384an9asb4kdp10e"
}

POST /v1/auth/sign-out

End the console sign-in and clear its cookie.

Who may call it: A console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

None.

Response body

None. The service answers 204.

POST /v1/auth/sign-in/link

Mail a single-use sign-in link. The answer is the same whether or not the address is known.

Who may call it: Anyone, with no credential.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

FieldTypeRequiredDescription
emailstringnoThe address to mail the sign-in link to.

Response body

None. The service answers 204.

Example request

JSON
{
  "email": "dev@example.com"
}

POST /v1/auth/sign-in/link/verify

Exchange the token out of a mailed sign-in link for a console sign-in, once.

Who may call it: Anyone, with no credential.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

FieldTypeRequiredDescription
tokenstringnoThe token out of the mailed link. It is good once.
idTokenstringnoThe Identity Platform ID token the console holds after completing the link (and the second factor, when one is enrolled) with the web SDK.

Response body

FieldTypeDescription
tokenstringbsk_cs_..., presented as Authorization: Bearer.
expiresAtstring (64-bit integer)Unix ms; identity.consoleTokenTtlS after issue.
userIdstringThe signed-in person's id.
mfaAtstring (64-bit integer)Unix ms this user last passed MFA; 0 when they never have.

Example request

JSON
{
  "token": "5m8qTzX..."
}

Example response

JSON
{
  "token": "bsk_cs_...",
  "expiresAt": "1774000900000",
  "userId": "user_2737116ccba906a9d2229ddfdbf5b812"
}

GET /v1/auth/sign-in/oidc/{provider}

Start a Google or Microsoft sign-in without the web SDK: the authorization URL, and a state cookie.

Who may call it: Anyone, with no credential.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
urlstringThe provider's authorization URL, carrying the state and the PKCE S256 challenge.

Example response

JSON
{
  "url": "https://accounts.google.com/o/oauth2/v2/auth?response_type=code&scope=openid+email+profile&state=Qm9...&code_challenge=E9Mr...&code_challenge_method=S256"
}

POST /v1/auth/sign-in/oidc/{provider}/callback

Finish a Google or Microsoft sign-in: the popup's two ID tokens for a console sign-in.

Who may call it: Anyone, with no credential.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
idTokenstringnoThe Identity Platform ID token the popup answered.
providerIdTokenstringnoThe provider's own ID token the popup answered (credential.idToken).
codestringnoThe authorization code, on the redirect shape only; empty on the popup shape.
statestringnoThe state the authorization URL carried, on the redirect shape only; it must equal the state cookie's.

Response body

FieldTypeDescription
tokenstringbsk_cs_..., presented as Authorization: Bearer.
expiresAtstring (64-bit integer)Unix ms; identity.consoleTokenTtlS after issue.
userIdstringThe signed-in person's id.
mfaAtstring (64-bit integer)Unix ms this user last passed MFA; 0 when they never have.

Example request

JSON
{
  "idToken": "eyJhbGciOi...",
  "providerIdToken": "eyJhbGciOi..."
}

Example response

JSON
{
  "token": "bsk_cs_...",
  "expiresAt": "1774000900000",
  "userId": "user_2737116ccba906a9d2229ddfdbf5b812"
}

GET /v1/auth/sign-in/providers

Which federated providers this deployment signs in with: Google and Microsoft, each enabled or not.

Who may call it: Anyone, with no credential.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

None.

Response body

FieldTypeDescription
providersarray of SignInProviderOne row per federated provider of the private MVP, google then microsoft.

Example response

JSON
{
  "providers": [
    {
      "name": "google",
      "enabled": true
    },
    {
      "name": "microsoft",
      "enabled": false
    }
  ]
}

POST /v1/auth/step-up

Pass the second factor again, so the console sign-in may do a step-up act.

Who may call it: A console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

FieldTypeRequiredDescription
idTokenstringnoThe Identity Platform ID token the console holds after passing the factor again with the web SDK.

Response body

FieldTypeDescription
tokenstringbsk_cs_..., presented as Authorization: Bearer.
expiresAtstring (64-bit integer)Unix ms; identity.consoleTokenTtlS after issue.
userIdstringThe signed-in person's id.
mfaAtstring (64-bit integer)Unix ms this user last passed MFA; 0 when they never have.

Example request

JSON
{
  "idToken": "eyJhbGciOi..."
}

Example response

JSON
{
  "token": "bsk_cs_...",
  "expiresAt": "1774000900000",
  "userId": "user_2737116ccba906a9d2229ddfdbf5b812",
  "mfaAt": "1773999990000"
}

POST /v1/auth/device/code

Start a terminal's sign-in: a device code to poll with, and a short code the person compares.

Who may call it: Anyone, with no credential.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

None.

Response body

FieldTypeDescription
deviceCodestringThe secret the terminal polls with.
userCodestringThe short code the human compares against the one on the approval page.
verificationUristringThe console page to open, with userCode already in it: one URL on one line, so the person has no second address to compare.
expiresInnumberSeconds this code is good for: identity.deviceCodeTtlS = 600.
intervalnumberSeconds between polls: identity.devicePollIntervalS = 5.

Example response

JSON
{
  "deviceCode": "kR3v8Yq...",
  "userCode": "BSKF-4QT9",
  "verificationUri": "https://console.example.com/auth/device?code=BSKF-4QT9",
  "expiresIn": 600,
  "interval": 5
}

POST /v1/auth/device/token

Poll a device code. It answers pending until somebody approves, then the console credentials.

Who may call it: Anyone, with no credential.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

FieldTypeRequiredDescription
deviceCodestringnoThe deviceCode of the answer above, sent back unchanged.

Response body

FieldTypeDescription
tokenstringbsk_cs_..., presented as Authorization: Bearer.
expiresAtstring (64-bit integer)Unix ms; identity.consoleTokenTtlS after issue.
userIdstringThe signed-in person's id.
mfaAtstring (64-bit integer)Unix ms this user last passed MFA; 0 when they never have.

Example request

JSON
{
  "deviceCode": "kR3v8Yq..."
}

Example response

JSON
{
  "token": "bsk_cs_...",
  "expiresAt": "1774000900000",
  "userId": "user_2737116ccba906a9d2229ddfdbf5b812"
}

POST /v1/auth/device/approve

Approve or cancel a terminal's sign-in, from the console page the person opened.

Who may call it: A console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

FieldTypeRequiredDescription
userCodestringnoThe short code the approval page is showing, which the person compared against the one in the terminal.
decisionstringnoapprove or deny, and nothing else: any other word is a validation refusal naming decision.

Response body

None. The service answers 204.

Example request

JSON
{
  "userCode": "BSKF-4QT9",
  "decision": "approve"
}

GET /v1/auth/device/{userCode}

Read a pending device code's time left, for the approval page's countdown.

Who may call it: A console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
expiresInnumberSeconds until the pending device code expires (RFC 8628, CALL-1192); an approved or spent code answers invalid_key instead.

Example response

JSON
{
  "expiresIn": 540
}

POST /v1/platform/mfa-resets

Remove the second factor of a console user who lost the device that holds it, and end their console sign-ins. Platform staff only, signed in with a fresh factor.

Who may call it: A console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

FieldTypeRequiredDescription
orgIdstringyesThe organization the console user is a member of, as org_....
emailstringyesThe console user's address.

Response body

None. The service answers 204.

Example request

JSON
{
  "orgId": "org_a14xc28ymkn7tx9qz8pv",
  "email": "dev@example.com"
}

Organizations

The organization a project belongs to, its projects, its organization keys, its plan and its policies.

GET /v1/orgs/{orgId}

Read one organization. The caller has to hold a relation on it.

Who may call it: An organization key carrying projects:read, or a console sign-in.

Permission: projects:read, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
orgIdstringThe organization's id.
namestringThe organization's name, as it is shown in the console.
createdAtstring (64-bit integer)Unix ms.

Example response

JSON
{
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "name": "Northwind",
  "createdAt": "1774000000000"
}

PATCH /v1/orgs/{orgId}

Rename an organization. Organization owners and admins only.

Who may call it: A console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
namestringyesThe organization's new name, 1-64 characters after trimming.

Response body

The OrgView object, written out under Objects.

Example request

JSON
{
  "name": "Northwind Games"
}

Example response

JSON
{
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "name": "Northwind Games",
  "createdAt": "1774000000000"
}

GET /v1/orgs/{orgId}/projects

List the organization's projects, with their settings.

Who may call it: An organization key carrying projects:read, or a console sign-in.

Permission: projects:read, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
projectsarray of ProjectViewOne project per row.

Example response

JSON
{
  "projects": [
    {
      "projectId": "proj_g1qr384an9asb4kdp10e",
      "orgId": "org_2f9wvkpcd87svnzcgkxs",
      "name": "Acme demo",
      "slug": "acme-demo",
      "createdAt": "1769990400000"
    }
  ]
}

POST /v1/orgs/{orgId}/projects

Create a project, with the default settings and limits. Answers the project, including the slug it will be called by.

Who may call it: An organization key carrying projects:create, or a console sign-in.

Permission: projects:create, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
namestringyesYour own name for the project, 1 to 64 characters. The slug is made from it.
slugstringnoThe public handle to claim.

Response body

The ProjectView object, written out under Objects.

Example request

JSON
{
  "name": "Acme demo",
  "slug": "acme-demo"
}

Example response

JSON
{
  "projectId": "proj_g1qr384an9asb4kdp10e",
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "name": "Acme demo",
  "slug": "acme-demo",
  "createdAt": "1774000000000",
  "e2ee": "off"
}

GET /v1/orgs/{orgId}/admin-keys

List the organization keys, masked, each with its expiry.

Who may call it: An organization key carrying keys:manage, or a console sign-in.

Permission: keys:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
keysarray of KeyViewOne organization key per row. A key's full string is never listed.

Example response

JSON
{
  "keys": [
    {
      "keyId": "key_r227gb9v7n49gcez",
      "type": "admin",
      "orgId": "org_2f9wvkpcd87svnzcgkxs",
      "masked": "bsk_ak_j2vzz9nz...najb",
      "status": "active",
      "label": "ci",
      "createdAt": "1769990400000",
      "expiresAt": "1777766400000"
    }
  ]
}

POST /v1/orgs/{orgId}/admin-keys

Create an organization key on the organization's service account.

Who may call it: An organization key carrying keys:manage, or a console sign-in.

Permission: keys:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
labelstringnoA name for the key, shown wherever the key is listed.
projectsarray of stringnoEmpty means every project in the organization.
expiresAtstring (64-bit integer)noWhen the key expires, Unix ms.
permissionsarray of stringyesThe permission list the organization key holds, from the one vocabulary: the list is the fence the role used to stand in for.
subjectsarray of stringnoAn organization key holding tokens:mint only, validated as CreateKeyRequest.subjects is: the subject prefixes a mint under it may name, as display:.
loginbooleannoTrue when bitskiff login or bitskiff init makes the login key after a person approved the sign-in: the key has no expiry unless expiresAt names one, and records that person as its approver.

Response body

The KeyView object, written out under Objects.

Example request

JSON
{
  "label": "console",
  "projects": [
    "proj_g1qr384an9asb4kdp10e"
  ],
  "permissions": [
    "sessions:open",
    "project:read"
  ]
}

Example response

JSON
{
  "keyId": "key_r227gb9v7n49gcez",
  "type": "admin",
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "projects": [
    "proj_g1qr384an9asb4kdp10e"
  ],
  "secret": "bsk_ak_...",
  "masked": "bsk_ak_j2vzz9nz...najb",
  "status": "active",
  "label": "console",
  "permissions": [
    "sessions:open",
    "project:read"
  ],
  "createdAt": "1774000000000"
}

DELETE /v1/orgs/{orgId}/admin-keys/{keyId}

Revoke an organization key before it expires. The revocation follows through to every key it created: reason=manual lets their live work drain, reason=compromise ends every session they opened at once.

Who may call it: An organization key carrying keys:manage, or a console sign-in.

Permission: keys:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Query parameters

These travel in the query string. This route takes no body.

ParameterTypeRequiredDescription
reasonstringno"manual" (the default) | "compromise". Anything else is refused validation with details.field reason.

Response body

The KeyView object, written out under Objects.

Example response

JSON
{
  "keyId": "key_r227gb9v7n49gcez",
  "type": "admin",
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "projects": [
    "proj_g1qr384an9asb4kdp10e"
  ],
  "masked": "bsk_ak_j2vzz9nz...najb",
  "status": "revoked",
  "label": "console",
  "createdAt": "1774000000000",
  "revokedAt": "1774000500000"
}

GET /v1/orgs

List the organizations the signed-in console user is a member of.

Who may call it: An organization key, or a console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

None.

Response body

FieldTypeDescription
orgsarray of OrgViewThe organizations the signed-in person is a member of, newest first.
staffbooleanTrue when the caller is an organization key that holds the staff role. Only the key holder reads it, and a console session never sees it set. bitskiff login refuses a key for which it is true.

Example response

JSON
{
  "orgs": [
    {
      "orgId": "org_2f9wvkpcd87svnzcgkxs",
      "name": "Northwind",
      "createdAt": "1774000000000"
    }
  ]
}

POST /v1/orgs

Create an organization.

Who may call it: A console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

FieldTypeRequiredDescription
namestringnoThe name to give the organization.
planPlanRequestnoThe organization's plan, required: staff name the plan every organization they create directly is bounded by.

Response body

The OrgView object, written out under Objects.

Example request

JSON
{
  "name": "Northwind"
}

Example response

JSON
{
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "name": "Northwind",
  "createdAt": "1774000000000"
}

GET /v1/orgs/{orgId}/plan

Read the organization's plan: its name, its ceilings, and when it expires.

Who may call it: An organization key carrying projects:read, or a console sign-in.

Permission: projects:read, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
orgIdstringThe organization the plan is on.
planOrgPlanThe plan on the organization's record: its name, its ceilings, and when it expires.
expiredbooleanWhether the plan has passed its expiry, read against the clock when the request was answered.

Example response

JSON
{
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "plan": {
    "name": "experimental",
    "ceilings": {
      "concurrentSessions": "5"
    },
    "expiresAt": "1776592000000",
    "setAt": "1774000000000",
    "setBy": "user_2737116ccba906a9d2229ddfdbf5b812"
  }
}

PUT /v1/orgs/{orgId}/plan

Set or renew the organization's plan. Platform staff only.

Who may call it: An organization key, or a console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
namestringyesThe named plan to set. Its template resolves into values on the organization's record.
ceilingsobject, string keys to string (64-bit integer)noOverrides on the template, keyed by the catalogue parameter (concurrentSessions), the one spelling on every wire surface; only rows a project may raise.
expiresAtstring (64-bit integer)noUnix ms.

Response body

FieldTypeDescription
orgIdstringThe organization the plan is on.
planOrgPlanThe plan on the organization's record: its name, its ceilings, and when it expires.
expiredbooleanWhether the plan has passed its expiry, read against the clock when the request was answered.

Example request

JSON
{
  "name": "experimental",
  "ceilings": {
    "concurrentSessions": "5"
  },
  "expiresAt": "1776592000000"
}

Example response

JSON
{
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "plan": {
    "name": "experimental",
    "ceilings": {
      "concurrentSessions": "5"
    },
    "expiresAt": "1776592000000",
    "setAt": "1774000000000",
    "setBy": "user_2737116ccba906a9d2229ddfdbf5b812"
  }
}

GET /v1/orgs/{orgId}/policies

Read the organization's policies: whether every member must enrol a second factor, whether platform staff may read its records, who makes organization keys, how long one may live, and whether a CLI sign-in acts here.

Who may call it: An organization key carrying projects:read, or a console sign-in.

Permission: projects:read, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
orgIdstringThe organization in the path, as org_....
mfaRequiredbooleanWhether every member must enrol a second factor; false by default.
staffAccessstringallowed (the default) or refused.
orgKeyMintingstringkeys (the default) or console.
keyMaxLifetimestringThe longest an organization key may live, as a duration; absent when none is set.
deviceSignInstringallowed (the default) or refused.
providerSecondFactorstringignore (the default) or accept: whether a provider's second factor counts in place of Bitskiff's.

Example response

JSON
{
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "staffAccess": "allowed"
}

PUT /v1/orgs/{orgId}/policies

Set the organization's policies: whether every member must enrol a second factor, whether platform staff may read its records, who makes organization keys, how long one may live, and whether a CLI sign-in acts here. Organization owners only.

Who may call it: A console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
mfaRequiredbooleannoWhether every member must enrol a second factor.
staffAccessstringyesWhether platform staff may read the organization's records: allowed or refused.
orgKeyMintingstringnoWho makes organization keys: keys (a key holding keys:manage, or a console sign-in) or console (a console sign-in only).
keyMaxLifetimestringnoThe longest an organization key made or rotated from now on may live, as a duration (2160h), at most a year; empty clears it.
deviceSignInstringnoWhether a console sign-in a device approval issued (bitskiff login, bitskiff init) acts in this organization: allowed or refused.
providerSecondFactorstringnoWhether a second factor a Google or Microsoft sign-in asserts (its amr holds mfa) counts in place of Bitskiff's own: ignore (the default) or accept.

Response body

FieldTypeDescription
orgIdstringThe organization in the path, as org_....
mfaRequiredbooleanWhether every member must enrol a second factor; false by default.
staffAccessstringallowed (the default) or refused.
orgKeyMintingstringkeys (the default) or console.
keyMaxLifetimestringThe longest an organization key may live, as a duration; absent when none is set.
deviceSignInstringallowed (the default) or refused.
providerSecondFactorstringignore (the default) or accept: whether a provider's second factor counts in place of Bitskiff's.

Example request

JSON
{
  "mfaRequired": true,
  "staffAccess": "refused"
}

Example response

JSON
{
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "mfaRequired": true,
  "staffAccess": "refused"
}

GET /v1/platform/orgs

Every organization on the platform, with its plan, its owners' addresses, its project count and what it is running now. Platform staff only.

Who may call it: An organization key, or a console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

Query parameters

These travel in the query string. This route takes no body.

ParameterTypeRequiredDescription
cursorstringnoWhere to carry on from: the nextCursor the page before this one answered with. It is opaque, and it belongs to this listing: one this service did not answer with, or one from another listing, is refused validation with details.field cursor rather than answering an empty page.
limitnumbernoHow many organizations this page may answer with, 1 to 200. Omit it for 50. Every paged route takes the same bound, and a value outside it is refused validation with details.field limit rather than shrunk.

Response body

FieldTypeDescription
orgsarray of PlatformOrgViewThis page of organizations, ordered by id.
nextCursorstringEmpty when this page is the last.
committedClientSlotsnumberThe client slots every live plan commits, summed over every organization and not only this page.
unboundedLivePlansnumberLive plans that bound no clients, so commit no counted slots.
platformSlotsnumberThe platform ceiling's slots, the number committedClientSlots is read against.
refusedOrgsnumberOrganizations whose owner refused staff access: counted over every organization, never listed on any page.

Example response

JSON
{
  "orgs": [
    {
      "orgId": "org_2f9wvkpcd87svnzcgkxs",
      "name": "Acme Games",
      "createdAt": "1774000000000",
      "ownerEmails": [
        "owner@example.com"
      ],
      "plan": {
        "name": "experimental",
        "ceilings": {
          "concurrentSessions": "5"
        },
        "expiresAt": "1776592000000",
        "setAt": "1774000000000",
        "setBy": "user_2737116ccba906a9d2229ddfdbf5b812"
      },
      "projectCount": 2,
      "liveSessions": 1,
      "liveClients": 4,
      "clientSlots": 40,
      "platformSlots": 1000
    }
  ],
  "committedClientSlots": 40,
  "unboundedLivePlans": 0,
  "platformSlots": 1000
}

GET /v1/platform/orgs/{orgId}/usage

One organization's month as cost per leg: the quantity, the unit cost to Bitskiff and their product, each labelled by what it is. Platform staff only.

Who may call it: An organization key, or a console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Query parameters

These travel in the query string. This route takes no body.

ParameterTypeRequiredDescription
monthstringnoThe UTC month to read, as YYYY-MM. Omit it for the current month. Anything else is refused validation with details.field month.

Response body

FieldTypeDescription
orgIdstringThe organization, as org_....
monthstringThe UTC month read, as YYYY-MM.
projectCountnumberHow many projects were summed.
legsarray of PlatformCostLegOne row per leg, in its order.
totalnumberThe sum of every leg's cost whose inTotal is set, in dollars.
totalKindstringWhat the total is: an estimate whenever any leg in it is one.
frozenThroughstring (64-bit integer)The start of the earliest month the rollups have frozen across the organization's projects, Unix ms; 0 when nothing is frozen.
floorstringThe line on the fixed floor, which the environment carries.

Example response

JSON
{
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "month": "2026-09",
  "projectCount": 2,
  "legs": [
    {
      "leg": "Session minutes",
      "quantity": 300,
      "unit": "session minutes",
      "quantityKind": "measured",
      "unitCost": 0.000428,
      "unitCostKind": "an assumption",
      "cost": 0.1284,
      "costKind": "an assumption",
      "inTotal": false,
      "note": "a second view of the floor; never added to the minute row the total carries (9.5)"
    },
    {
      "leg": "Engine data bytes sent",
      "quantity": 0.5,
      "unit": "GB",
      "quantityKind": "measured",
      "unitCost": 0.12,
      "unitCostKind": "a parameter",
      "cost": 0.06,
      "costKind": "an estimate",
      "inTotal": true,
      "note": "every engine byte taken as internet egress; some stays inside Google's network (9.3)"
    }
  ],
  "total": 0.06,
  "totalKind": "an estimate",
  "frozenThrough": "1788220800000",
  "floor": "the fixed floor is carried by the environment, not by this organization"
}

Members

Who is in an organization and with which role, a change of role, and a removal, which ends their roles on the organization and its projects and signs them out of the console.

GET /v1/orgs/{orgId}/members

Every member of the organization: the console user, their address, their role and when they joined.

Who may call it: An organization key carrying projects:read, or a console sign-in.

Permission: projects:read, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
membersarray of MemberViewOne row per member, owners first and then by address.

Example response

JSON
{
  "members": [
    {
      "userId": "user_2737116ccba906a9d2229ddfdbf5b812",
      "email": "owner@example.com",
      "role": "org_owner",
      "relations": [
        "owner"
      ],
      "joinedAt": "1774000000000",
      "caller": true
    },
    {
      "userId": "user_9c41d0e7a3b25f86c1d4e09b7a63f215",
      "email": "dev@example.com",
      "role": "org_member",
      "relations": [
        "member"
      ],
      "joinedAt": "1774000600000"
    }
  ]
}

PATCH /v1/orgs/{orgId}/members/{userId}

Change a member's organization role. Their project roles stay as they are.

Who may call it: An organization key carrying members:manage, or a console sign-in.

Permission: members:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
rolestringyesorg_owner, org_admin, billing_admin or org_member.

Response body

The MemberView object, written out under Objects.

Example request

JSON
{
  "role": "org_admin"
}

Example response

JSON
{
  "userId": "user_9c41d0e7a3b25f86c1d4e09b7a63f215",
  "email": "dev@example.com",
  "role": "org_admin",
  "relations": [
    "admin"
  ],
  "joinedAt": "1774000600000"
}

DELETE /v1/orgs/{orgId}/members/{userId}

Remove a member: their relations on the organization and its projects end, and so do their console sign-ins.

Who may call it: An organization key carrying members:manage, or a console sign-in.

Permission: members:manage, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

The MemberView object, written out under Objects.

Example response

JSON
{
  "userId": "user_9c41d0e7a3b25f86c1d4e09b7a63f215",
  "email": "dev@example.com",
  "role": "org_admin",
  "relations": [
    "admin"
  ],
  "joinedAt": "1774000600000"
}

Billing accounts

An organization's billing accounts, the projects attached to each, a month's usage rolled up per billing account, and each billing account's invoices, their PDFs and its invoice webhooks. Platform staff issue an ended month at once and mark an invoice paid or void.

GET /v1/orgs/{orgId}/billing-accounts

The organization's billing accounts the caller may view, the default first, each with the projects attached to it.

Who may call it: An organization key carrying billing:read, or a console sign-in.

Permission: billing:read, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
billingAccountsarray of BillingAccountViewThe default first, then by creation.

Example response

JSON
{
  "billingAccounts": [
    {
      "billingAccountId": "bill_q2xm9pbz3tvn8wcd7g4k",
      "orgId": "org_2f9wvkpcd87svnzcgkxs",
      "name": "Default",
      "isDefault": true,
      "createdAt": "1774000000000",
      "projectIds": [
        "proj_k8m2p4x7q9z3v5w6t1yn"
      ]
    },
    {
      "billingAccountId": "bill_8wcd7g4kq2xm9pbz3tvn",
      "orgId": "org_2f9wvkpcd87svnzcgkxs",
      "name": "Agency",
      "isDefault": false,
      "legalName": "Agency Ltd",
      "billingEmail": "accounts@agency.example",
      "createdAt": "1774000600000"
    }
  ]
}

POST /v1/orgs/{orgId}/billing-accounts

Create a billing account. It is not the default; attach projects to it one by one.

Who may call it: A console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
namestringyes1-64 characters, not unique.
legalNamestringnoThe billed party's legal name.
billingEmailstringnoThe address invoices are sent to.
addressstringnoThe billed party's postal address, free text.
taxIdstringnoThe billed party's tax identifier, free text.

Response body

The BillingAccountView object, written out under Objects.

Example request

JSON
{
  "name": "Agency",
  "legalName": "Agency Ltd",
  "billingEmail": "accounts@agency.example"
}

Example response

JSON
{
  "billingAccountId": "bill_8wcd7g4kq2xm9pbz3tvn",
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "name": "Agency",
  "isDefault": false,
  "legalName": "Agency Ltd",
  "billingEmail": "accounts@agency.example",
  "createdAt": "1774000600000"
}

GET /v1/orgs/{orgId}/billing-accounts/{billingAccountId}

One billing account, with the projects attached to it.

Who may call it: An organization key carrying billing:read, or a console sign-in.

Permission: billing:read, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

The BillingAccountView object, written out under Objects.

Example response

JSON
{
  "billingAccountId": "bill_q2xm9pbz3tvn8wcd7g4k",
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "name": "Default",
  "isDefault": true,
  "createdAt": "1774000000000",
  "projectIds": [
    "proj_k8m2p4x7q9z3v5w6t1yn"
  ]
}

PATCH /v1/orgs/{orgId}/billing-accounts/{billingAccountId}

Rename a billing account, set the billed party's details, or make it the organization's default.

Who may call it: A console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
namestringno1-64 characters.
isDefaultbooleannotrue makes this billing account the organization's default and the old default an ordinary one; false is refused, because an organization always keeps a default: make another the default instead.
legalNamestringnoAn empty string clears it.
billingEmailstringnoAn empty string clears it.
addressstringnoAn empty string clears it.
taxIdstringnoAn empty string clears it.

Response body

The BillingAccountView object, written out under Objects.

Example request

JSON
{
  "isDefault": true,
  "taxId": "GB123456789"
}

Example response

JSON
{
  "billingAccountId": "bill_8wcd7g4kq2xm9pbz3tvn",
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "name": "Agency",
  "isDefault": true,
  "legalName": "Agency Ltd",
  "billingEmail": "accounts@agency.example",
  "taxId": "GB123456789",
  "createdAt": "1774000600000"
}

GET /v1/orgs/{orgId}/billing-accounts/{billingAccountId}/usage

A billing account's month roll-up: each attached project's owned meters for one UTC month, and their totals.

Who may call it: An organization key carrying billing:read, or a console sign-in.

Permission: billing:read, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Query parameters

These travel in the query string. This route takes no body.

ParameterTypeRequiredDescription
monthstringnoThe UTC month to read, as YYYY-MM. Omit it for the current month. Anything else is refused validation with details.field month.

Response body

FieldTypeDescription
billingAccountIdstringThe billing account rolled up.
orgIdstringIts organization.
monthstringThe UTC month read, as YYYY-MM.
linesarray of BillingUsageLineOne line per project and owned meter, by project and then in the meter order above; a project contributes only the days it was attached to this billing account.
totalsarray of BillingMeterTotalOne row per owned meter, in the same order.
frozenThroughstring (64-bit integer)The start of the earliest month the rollups have frozen across the projects read, Unix ms; 0 when nothing is frozen.

Example response

JSON
{
  "billingAccountId": "bill_q2xm9pbz3tvn8wcd7g4k",
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "month": "2026-09",
  "lines": [
    {
      "projectId": "proj_k8m2p4x7q9z3v5w6t1yn",
      "meter": "client_minutes",
      "leg": "Client minutes",
      "quantity": 1240.5,
      "unit": "client minutes"
    },
    {
      "projectId": "proj_k8m2p4x7q9z3v5w6t1yn",
      "meter": "session_minutes",
      "leg": "Session minutes",
      "quantity": 310,
      "unit": "session minutes"
    },
    {
      "projectId": "proj_k8m2p4x7q9z3v5w6t1yn",
      "meter": "relay_bytes",
      "leg": "Relay bytes for direct links",
      "quantity": 52428800,
      "unit": "bytes"
    },
    {
      "projectId": "proj_k8m2p4x7q9z3v5w6t1yn",
      "meter": "data_bytes_sent",
      "leg": "Engine data bytes sent",
      "quantity": 734003200,
      "unit": "bytes"
    }
  ],
  "totals": [
    {
      "meter": "client_minutes",
      "leg": "Client minutes",
      "quantity": 1240.5,
      "unit": "client minutes"
    },
    {
      "meter": "session_minutes",
      "leg": "Session minutes",
      "quantity": 310,
      "unit": "session minutes"
    },
    {
      "meter": "relay_bytes",
      "leg": "Relay bytes for direct links",
      "quantity": 52428800,
      "unit": "bytes"
    },
    {
      "meter": "data_bytes_sent",
      "leg": "Engine data bytes sent",
      "quantity": 734003200,
      "unit": "bytes"
    }
  ],
  "frozenThrough": "1788220800000"
}

POST /v1/projects/{projectId}/billing-account

Attach a project to another billing account of its organization. Its usage rolls up there from the start of that UTC day.

Who may call it: A console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
billingAccountIdstringyesA bill_ id of the project's own organization.

Response body

FieldTypeDescription
projectIdstringThe project, as proj_....
billingAccountIdstringThe billing account it is attached to now.
previousBillingAccountIdstringThe one it was attached to before; empty when it had none.

Example request

JSON
{
  "billingAccountId": "bill_8wcd7g4kq2xm9pbz3tvn"
}

Example response

JSON
{
  "projectId": "proj_k8m2p4x7q9z3v5w6t1yn",
  "billingAccountId": "bill_8wcd7g4kq2xm9pbz3tvn",
  "previousBillingAccountId": "bill_q2xm9pbz3tvn8wcd7g4k"
}

GET /v1/orgs/{orgId}/billing-accounts/{billingAccountId}/invoices

A billing account's invoices, newest month first: number, period, status, total and currency. Paged.

Who may call it: An organization key carrying billing:read, or a console sign-in.

Permission: billing:read, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Query parameters

These travel in the query string. This route takes no body.

ParameterTypeRequiredDescription
cursorstringnoWhere to carry on from: the nextCursor the page before this one answered with. It is opaque, and it belongs to this listing: one this service did not answer with, or one from another listing, is refused validation with details.field cursor rather than answering an empty page.
limitnumbernoHow many invoices this page may answer with, 1 to 200. Omit it for 50. Every paged route takes the same bound, and a value outside it is refused validation with details.field limit rather than shrunk.

Response body

FieldTypeDescription
invoicesarray of InvoiceViewHeaders: lines is empty on each.
nextCursorstringEmpty when this page is the last.

Example response

JSON
{
  "invoices": [
    {
      "invoiceId": "invc_7g4kq2xm9pbz3tvn8wcd",
      "billingAccountId": "bill_q2xm9pbz3tvn8wcd7g4k",
      "orgId": "org_2f9wvkpcd87svnzcgkxs",
      "period": "2026-09",
      "periodStart": "1788220800000",
      "periodEnd": "1790812800000",
      "status": "paid",
      "number": "Q2XM9PBZ-0001",
      "rateCardVersion": 1,
      "currency": "USD",
      "accountName": "Default",
      "issuedAt": "1790992800000",
      "paidAt": "1790992800000",
      "statusReason": "zero_total",
      "pdfSha256": "<64 hex characters>",
      "frozenThrough": "1790812800000"
    }
  ]
}

GET /v1/orgs/{orgId}/billing-accounts/{billingAccountId}/invoices/{invoiceId}

One invoice with its line items and the rate card version it was rated on.

Who may call it: An organization key carrying billing:read, or a console sign-in.

Permission: billing:read, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

The InvoiceView object, written out under Objects.

Example response

JSON
{
  "invoiceId": "invc_7g4kq2xm9pbz3tvn8wcd",
  "billingAccountId": "bill_q2xm9pbz3tvn8wcd7g4k",
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "period": "2026-09",
  "periodStart": "1788220800000",
  "periodEnd": "1790812800000",
  "status": "paid",
  "number": "Q2XM9PBZ-0001",
  "rateCardVersion": 1,
  "currency": "USD",
  "lines": [
    {
      "lineNo": 1,
      "kind": "usage",
      "projectId": "proj_k8m2p4x7q9z3v5w6t1yn",
      "meter": "client_minutes",
      "leg": "Client minutes",
      "quantity": 1240.5,
      "unit": "client minutes"
    },
    {
      "lineNo": 2,
      "kind": "usage",
      "projectId": "proj_k8m2p4x7q9z3v5w6t1yn",
      "meter": "data_bytes_sent",
      "leg": "Engine data bytes sent",
      "quantity": 734003200,
      "unit": "bytes"
    },
    {
      "lineNo": 3,
      "kind": "fee",
      "meter": "fee_monthly",
      "leg": "Monthly fee (rate card parameter)",
      "quantity": 1,
      "unit": "month"
    }
  ],
  "accountName": "Default",
  "issuedAt": "1790992800000",
  "paidAt": "1790992800000",
  "statusReason": "zero_total",
  "pdfSha256": "<64 hex characters>",
  "frozenThrough": "1790812800000"
}

GET /v1/orgs/{orgId}/billing-accounts/{billingAccountId}/invoices/{invoiceId}/pdf

The invoice's archived PDF, as application/pdf.

Who may call it: An organization key carrying billing:read, or a console sign-in.

Permission: billing:read, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

None. The service answers 204.

GET /v1/orgs/{orgId}/billing-accounts/{billingAccountId}/webhooks

Read where the billing account has its invoice events delivered, and the key id a delivery is signed with. The signing secret is never returned here.

Who may call it: An organization key carrying billing:read, or a console sign-in.

Permission: billing:read, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
urlstringThe https URL; empty when none is set.
secretstringHex, returned by the PUT that derives it and always empty on the GET; the value a receiver recomputes the signature with.
kidstringThe name of the signing key version, as X-Bitskiff-Signature-Kid carries it.
kindsarray of stringThe invoice kinds the endpoint subscribes to: invoice.issued, invoice.paid and invoice.voided, each opt-in; empty is none.

Example response

JSON
{
  "url": "https://example.com/hooks/billing",
  "kid": "a1b2c3d4",
  "kinds": [
    "invoice.issued",
    "invoice.paid",
    "invoice.voided"
  ]
}

PUT /v1/orgs/{orgId}/billing-accounts/{billingAccountId}/webhooks

Set or clear the billing account's delivery URL and choose its invoice kinds. This response is the one place the signing secret is ever shown.

Who may call it: A console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
urlstringnoThe https URL deliveries go to, checked against the webhook URL policy.
kindsStringListPatchnoAbsent leaves the subscription as it was; present replaces it, and present with no values clears it.

Response body

FieldTypeDescription
urlstringThe https URL; empty when none is set.
secretstringHex, returned by the PUT that derives it and always empty on the GET; the value a receiver recomputes the signature with.
kidstringThe name of the signing key version, as X-Bitskiff-Signature-Kid carries it.
kindsarray of stringThe invoice kinds the endpoint subscribes to: invoice.issued, invoice.paid and invoice.voided, each opt-in; empty is none.

Example request

JSON
{
  "url": "https://example.com/hooks/billing",
  "kinds": {
    "values": [
      "invoice.issued",
      "invoice.paid",
      "invoice.voided"
    ]
  }
}

Example response

JSON
{
  "url": "https://example.com/hooks/billing",
  "secret": "<64 hex characters>",
  "kid": "a1b2c3d4",
  "kinds": [
    "invoice.issued",
    "invoice.paid",
    "invoice.voided"
  ]
}

POST /v1/platform/orgs/{orgId}/invoices

Issue an organization's invoices for one ended month now, rather than on the monthly pass. Platform staff only.

Who may call it: An organization key, or a console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
monthstringnoThe UTC month, YYYY-MM, which must have ended; empty is the month before the current one.

Response body

FieldTypeDescription
invoicesarray of InvoiceViewHeaders: lines is empty on each.
nextCursorstringEmpty when this page is the last.

Example request

JSON
{
  "month": "2026-09"
}

Example response

JSON
{
  "invoices": [
    {
      "invoiceId": "invc_7g4kq2xm9pbz3tvn8wcd",
      "billingAccountId": "bill_q2xm9pbz3tvn8wcd7g4k",
      "orgId": "org_2f9wvkpcd87svnzcgkxs",
      "period": "2026-09",
      "periodStart": "1788220800000",
      "periodEnd": "1790812800000",
      "status": "paid",
      "number": "Q2XM9PBZ-0001",
      "rateCardVersion": 1,
      "currency": "USD",
      "accountName": "Default",
      "issuedAt": "1790992800000",
      "paidAt": "1790992800000",
      "statusReason": "zero_total",
      "pdfSha256": "<64 hex characters>",
      "frozenThrough": "1790812800000"
    }
  ]
}

POST /v1/platform/orgs/{orgId}/invoices/{invoiceId}/status

Mark an issued invoice paid, or void an issued or paid one, with a reason. Platform staff only.

Who may call it: An organization key, or a console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
statusstringyespaid or void.
reasonstringyes1-200 characters.

Response body

The InvoiceView object, written out under Objects.

Example request

JSON
{
  "status": "void",
  "reason": "issued for a walk-through"
}

Example response

JSON
{
  "invoiceId": "invc_7g4kq2xm9pbz3tvn8wcd",
  "billingAccountId": "bill_q2xm9pbz3tvn8wcd7g4k",
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "period": "2026-09",
  "periodStart": "1788220800000",
  "periodEnd": "1790812800000",
  "status": "void",
  "number": "Q2XM9PBZ-0001",
  "rateCardVersion": 1,
  "currency": "USD",
  "accountName": "Default",
  "issuedAt": "1790992800000",
  "paidAt": "1790992800000",
  "voidedAt": "1791000000000",
  "statusReason": "issued for a walk-through",
  "pdfSha256": "<64 hex characters>",
  "frozenThrough": "1790812800000"
}

Invitations

Invite someone into an organization, read what is open, and revoke one. An invitation is accepted in the console, by the person who was invited. Platform staff invite someone to the platform the same way, with the plan the organization they create will carry.

GET /v1/orgs/{orgId}/invitations

Every invitation the organization holds, newest first, each with the state the clock puts it in.

Who may call it: An organization key carrying members:invite, or a console sign-in.

Permission: members:invite, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
invitationsarray of InvitationViewOne invitation per row, newest first.

Example response

JSON
{
  "invitations": [
    {
      "invitationId": "inv_rdbbjmyewanr9aa600qw",
      "orgId": "org_2f9wvkpcd87svnzcgkxs",
      "email": "dev@example.com",
      "roles": [
        "org_member"
      ],
      "invitedBy": "user_2737116ccba906a9d2229ddfdbf5b812",
      "createdAt": "1774000000000",
      "expiresAt": "1774604800000",
      "state": "open"
    }
  ]
}

POST /v1/orgs/{orgId}/invitations

Invite someone into the organization by email. A key may invite only at or below its own standing.

Who may call it: An organization key carrying members:invite, or a console sign-in.

Permission: members:invite, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

FieldTypeRequiredDescription
emailstringyesThe address to invite.
rolesarray of stringyesThe roles accepting the invitation writes.
projectIdstringnoEmpty unless a role in roles is a project role.

Response body

The InvitationView object, written out under Objects.

Example request

JSON
{
  "email": "dev@example.com",
  "roles": [
    "org_member"
  ]
}

Example response

JSON
{
  "invitationId": "inv_rdbbjmyewanr9aa600qw",
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "email": "dev@example.com",
  "roles": [
    "org_member"
  ],
  "invitedBy": "user_2737116ccba906a9d2229ddfdbf5b812",
  "createdAt": "1774000000000",
  "expiresAt": "1774604800000",
  "state": "open"
}

DELETE /v1/orgs/{orgId}/invitations/{invitationId}

Revoke an open invitation. The link it mailed stops working from the next request.

Who may call it: An organization key carrying members:invite, or a console sign-in.

Permission: members:invite, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

The InvitationView object, written out under Objects.

Example response

JSON
{
  "invitationId": "inv_rdbbjmyewanr9aa600qw",
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "email": "dev@example.com",
  "roles": [
    "org_member"
  ],
  "createdAt": "1774000000000",
  "revokedAt": "1774000500000",
  "state": "revoked"
}

POST /v1/invitations/accept

Accept an invitation with the token from its mail, as the signed-in console user.

Who may call it: A console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

FieldTypeRequiredDescription
tokenstringnoThe token out of the invitation mail. It is good once.

Response body

FieldTypeDescription
orgIdstringThe organization that was joined.
orgNamestringIts name.
rolesarray of stringThe relations the invitation carried, now held by the person who accepted it.

Example request

JSON
{
  "token": "q7Rk2mWv..."
}

Example response

JSON
{
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "orgName": "Northwind",
  "roles": [
    "org_member"
  ]
}

GET /v1/platform/invitations

Every platform invitation, newest first, each with the state the clock puts it in. Platform staff only.

Who may call it: An organization key, or a console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

None.

Response body

FieldTypeDescription
invitationsarray of InvitationViewOne invitation per row, newest first.

Example response

JSON
{
  "invitations": [
    {
      "invitationId": "inv_a14xc28ymkn7tx9qz8pv",
      "email": "dev@example.com",
      "invitedBy": "user_2737116ccba906a9d2229ddfdbf5b812",
      "createdAt": "1774000000000",
      "expiresAt": "1776592000000",
      "state": "open",
      "kind": "platform",
      "plan": {
        "name": "experimental",
        "expiresAt": "1776592000000"
      }
    }
  ]
}

POST /v1/platform/invitations

Invite a developer to the platform by email, with a plan. Accepting it creates their organization. Platform staff only.

Who may call it: An organization key, or a console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

FieldTypeRequiredDescription
emailstringyesThe address to invite to the platform.
planPlanRequestyesThe plan the organization created on acceptance will carry. plan.expiresAt is required when the plan is experimental.

Response body

The InvitationView object, written out under Objects.

Example request

JSON
{
  "email": "dev@example.com",
  "plan": {
    "name": "experimental",
    "expiresAt": "1776592000000"
  }
}

Example response

JSON
{
  "invitationId": "inv_a14xc28ymkn7tx9qz8pv",
  "email": "dev@example.com",
  "invitedBy": "user_2737116ccba906a9d2229ddfdbf5b812",
  "createdAt": "1774000000000",
  "expiresAt": "1776592000000",
  "state": "open",
  "kind": "platform",
  "plan": {
    "name": "experimental",
    "expiresAt": "1776592000000"
  }
}

DELETE /v1/platform/invitations/{invitationId}

Revoke a pending platform invitation. The link it mailed stops working from the next request. Platform staff only.

Who may call it: An organization key, or a console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

The InvitationView object, written out under Objects.

Example response

JSON
{
  "invitationId": "inv_a14xc28ymkn7tx9qz8pv",
  "email": "dev@example.com",
  "invitedBy": "user_2737116ccba906a9d2229ddfdbf5b812",
  "createdAt": "1774000000000",
  "expiresAt": "1776592000000",
  "revokedAt": "1774000500000",
  "state": "revoked",
  "kind": "platform",
  "plan": {
    "name": "experimental",
    "expiresAt": "1776592000000"
  }
}

Webhooks

Where a project has its events delivered, and the same events read back.

GET /v1/project/events

The project's event stream: the same objects the webhook delivers, kept for seven days or 10,000 entries and read forward from a cursor.

Who may call it: A secret key or organization key carrying usage:read, or a console sign-in.

Permission: usage:read, on a credential that carries permissions at all.

Query parameters

These travel in the query string, not in a body.

FieldTypeRequiredDescription
cursorstringnoWhere to read from: the nextCursor of the page before this one. Without it the page starts at the oldest event still kept, which is seven days back. It is opaque, and it belongs to this listing and this project: one this service did not answer with, or one from another listing, is refused validation with details.field cursor.
limitnumbernoHow many events this page may answer with, 1 to 200. Omit it for 50. Every paged route takes the same bound, and a value outside it is refused validation with details.field limit rather than shrunk.

Response body

FieldTypeDescription
eventsarray of ProjectEventThe events, oldest first, as far as this page goes.
nextCursorstringWhere the next page starts. Send it back as cursor. It is absent when this page is the last, so read the pages until it stops coming rather than until it is empty.
trimmedbooleanTrue when the cursor asked for is older than the oldest event the stream still keeps (seven days or 10,000 events): the page starts at the oldest kept, and what fell between is read from the ledger route instead.

Example query

HTTP
GET /v1/project/events?limit=2

Example response

JSON
{
  "events": [
    {
      "id": "m4tq2ow7zkc3xbv5ernh6jyd2a",
      "type": "open.refused",
      "at": "1774915200000",
      "data": "{\"orgId\":\"org_2f9wvkpcd87svnzcgkxs\",\"keyId\":\"key_y090cqev9fn21nxp\",\"label\":\"lobby-north\",\"code\":\"quota_exceeded\",\"clears\":\"waiting\",\"limit\":\"monthlySpendCap\",\"scope\":\"project\",\"scopeId\":\"proj_7hq2m9xk4rz3tv8bn6cw\",\"value\":1200,\"ceiling\":1000,\"resetAt\":1775001600000,\"requestId\":\"3f1b6a52-9c4d-4f0a-8b21-6de0c2f4a917\",\"count\":1}"
    },
    {
      "id": "u2fq6nz3wk7mcx4tbr5dh2pyga",
      "type": "open.refused",
      "at": "1774915500000",
      "data": "{\"orgId\":\"org_2f9wvkpcd87svnzcgkxs\",\"keyId\":\"key_y090cqev9fn21nxp\",\"code\":\"concurrency_limit\",\"clears\":\"waiting\",\"limit\":\"concurrentSessions\",\"scope\":\"project\",\"scopeId\":\"proj_7hq2m9xk4rz3tv8bn6cw\",\"value\":5,\"ceiling\":5,\"retryAfterMs\":10000,\"requestId\":\"8c07d4e1-5b3a-4c96-9f2d-1a7e50b8c364\",\"count\":240}"
    }
  ],
  "nextCursor": "1774915500000:u2fq6nz3wk7mcx4tbr5dh2pyga"
}

GET /v1/project/webhooks

Read where the project has its events delivered, and the key id a delivery is signed with. The signing secret is never returned here.

Who may call it: A secret key or organization key carrying webhooks:manage, or a console sign-in.

Permission: webhooks:manage, on a credential that carries permissions at all.

Request body

None.

Response body

FieldTypeDescription
urlstringWhere deliveries are posted.
secretstringHex, returned by the PUT that derives it and always empty on the GET; the value a receiver recomputes the MAC with.
kidstringWhich signing secret a delivery was signed with. It travels on the X-Bitskiff-Signature-Kid header, so a rotation is readable at the receiver.
kindsarray of stringThe opt-in kinds the project subscribes to: session.ended and client.gone, the usage records, and limits.raise_approved and limits.raise_denied, the outcome of a pending raise.

Example response

JSON
{
  "url": "https://example.com/hooks/bitskiff",
  "kid": "a1b2c3d4"
}

PUT /v1/project/webhooks

Set or clear the delivery URL. This response is the one place the signing secret is ever shown.

Who may call it: A secret key or organization key carrying webhooks:manage, or a console sign-in.

Permission: webhooks:manage, on a credential that carries permissions at all.

Request body

FieldTypeRequiredDescription
urlstringnoA new URL for deliveries. The empty string clears the webhook.
kindsStringListPatchnoAbsent leaves the subscription as it was; present replaces it, and present with no values clears it.

Response body

FieldTypeDescription
urlstringWhere deliveries are posted.
secretstringHex, returned by the PUT that derives it and always empty on the GET; the value a receiver recomputes the MAC with.
kidstringWhich signing secret a delivery was signed with. It travels on the X-Bitskiff-Signature-Kid header, so a rotation is readable at the receiver.
kindsarray of stringThe opt-in kinds the project subscribes to: session.ended and client.gone, the usage records, and limits.raise_approved and limits.raise_denied, the outcome of a pending raise.

Example request

JSON
{
  "url": "https://example.com/hooks/bitskiff"
}

Example response

JSON
{
  "url": "https://example.com/hooks/bitskiff",
  "secret": "<64 hex characters>",
  "kid": "a1b2c3d4"
}

SDK releases

Which SDK releases ran in the last 30 days: per project, rolled up to the organization, and across the platform for staff. And the sha256 of every SDK download of a release.

GET /v1/project/sdk-inventory

Read the SDK releases the project ran in the last 30 days: each package and version, the protocol version it negotiated, when it was last seen and how many sessions it served.

Who may call it: A secret key or organization key carrying project:read, or a console sign-in.

Permission: project:read, on a credential that carries permissions at all.

Request body

None.

Response body

FieldTypeDescription
rowsarray of SdkRowOne row per package, version and protocol version.
projectIdstringThe project the credential named.

Example response

JSON
{
  "rows": [
    {
      "sdk": "ts-host/0.2.1",
      "protocol": 1,
      "lastSeen": "1774000000000",
      "sessions30d": "42",
      "package": "ts-host",
      "version": "0.2.1",
      "protocolStatus": "current"
    }
  ],
  "projectId": "proj_g1qr384an9asb4kdp10e"
}

GET /v1/orgs/{orgId}/sdk-releases

Read the SDK releases the organization's projects ran in the last 30 days, rolled up over every project.

Who may call it: An organization key carrying projects:read, or a console sign-in.

Permission: projects:read, on a credential that carries permissions at all.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
orgIdstringThe organization in the path.
rowsarray of SdkRowOne row per package, version and protocol version.

Example response

JSON
{
  "orgId": "org_2f9wvkpcd87svnzcgkxs",
  "rows": [
    {
      "sdk": "ts-host/0.2.1",
      "protocol": 1,
      "lastSeen": "1774000000000",
      "sessions30d": "57",
      "package": "ts-host",
      "version": "0.2.1",
      "protocolStatus": "current"
    }
  ]
}

GET /v1/platform/sdk-releases

Every SDK release any organization ran in the last 30 days, with the organizations that ran it. Platform staff only.

Who may call it: An organization key, or a console sign-in.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

None.

Response body

FieldTypeDescription
releasesarray of PlatformSdkReleaseOrdered by package, then version, then protocol version.

Example response

JSON
{
  "releases": [
    {
      "release": {
        "sdk": "ts-host/0.2.1",
        "protocol": 1,
        "lastSeen": "1774000000000",
        "sessions30d": "120",
        "package": "ts-host",
        "version": "0.2.1",
        "protocolStatus": "current"
      },
      "orgIds": [
        "org_2f9wvkpcd87svnzcgkxs"
      ],
      "orgs": [
        {
          "orgId": "org_2f9wvkpcd87svnzcgkxs"
        }
      ]
    }
  ]
}

GET /v1/releases/{version}/digests

Every SDK download of one release with the sha256 of its bytes, so a download from the docs site can be checked against the service.

Who may call it: Anyone, with no credential.

Permission: none. The credential class above is the whole of what this route asks for.

An id in this path that the caller cannot reach is refused not_found; see Errors.

Request body

None. This route reads what it needs from the path.

Response body

FieldTypeDescription
versionstringThe release, X.Y.Z.
digestsarray of ReleaseDigestOne per SDK tarball, ordered by package.
recordedAtstring (64-bit integer)Unix ms the cut recorded them.

Example response

JSON
{
  "version": "0.2.2",
  "digests": [
    {
      "package": "@bitskiff/client",
      "file": "0.2.2/bitskiff-client.tgz",
      "sha256": "3f1c9a5e0b7d24c8e6f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f7",
      "bytes": "48213"
    },
    {
      "package": "@bitskiff/host",
      "file": "0.2.2/bitskiff-host.tgz",
      "sha256": "9a8b7c6d5e4f30211f2e3d4c5b6a79880a1b2c3d4e5f60718293a4b5c6d7e8f9",
      "bytes": "102944"
    }
  ],
  "recordedAt": "1774000000000"
}

Audit

Every key, limit and webhook change, with who made it.

GET /v1/project/audit

The project's audit log: one entry per key, limit and webhook change, with who made it. Filtered and paged.

Who may call it: An organization key carrying audit:read, or a console sign-in.

Permission: audit:read, on a credential that carries permissions at all.

Query parameters

These travel in the query string, not in a body.

FieldTypeRequiredDescription
actionstringnoKeep only the rows with this action, such as key.created. Omit it for every action.
actorstringnoKeep only the rows this actor wrote. Omit it for every actor.
sincestring (64-bit integer)noMs since epoch, inclusive.
untilstring (64-bit integer)noMs since epoch, exclusive.
cursorstringnoWhere to carry on from: the nextCursor the page before this one answered with. It is opaque, and it belongs to this listing and this project: one this service did not answer with, or one from another listing, is refused validation with details.field cursor.
limitnumbernoHow many rows this page may answer with, 1 to 200. Omit it for 50. Every paged route takes the same bound, and a value outside it is refused validation with details.field limit rather than shrunk.

Response body

FieldTypeDescription
eventsarray of AuditEventOne audited change per row.
nextCursorstringWhere the next page starts. Send it back as cursor. It is absent when this page is the last, so read the pages until it stops coming rather than until it is empty.

Example query

HTTP
GET /v1/project/audit?action=key.created&limit=20

Example response

JSON
{
  "events": [
    {
      "id": "evt_f2k9t2tc3a2vvtym54as",
      "at": "1774000000000",
      "orgId": "org_2f9wvkpcd87svnzcgkxs",
      "projectId": "proj_g1qr384an9asb4kdp10e",
      "actor": {
        "type": "user",
        "id": "user_2737116ccba906a9d2229ddfdbf5b812"
      },
      "action": "key.created",
      "target": {
        "type": "key",
        "id": "key_hn76yrbsbdghwpdv"
      },
      "after": {
        "label": "backend"
      },
      "requestId": "dcea9d32-21c0-4aac-8040-d9f90646ecc3"
    }
  ]
}

Service

What this endpoint is, and what it speaks. Neither needs a credential.

GET /v1/whoami

What this endpoint is: which region answers here, which environment it belongs to, and the address a session's live connection goes to.

Who may call it: Anyone, with no credential.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

None.

Response body

FieldTypeDescription
engineUrlstringWhere this region's transport connects, and the same value a Ticket carries as endpoint. Hand it to the SDK as it is: the transport settles the scheme and the path it needs. It is not an address to open a socket on yourself.
regionstringThe region that answered this call.
envstringWhich environment this endpoint belongs to: local, dev or prod.
apiIdstringThe sender identity the service itself signs with in this region, a_<region>: the same value a ticket carries, offered here for support.

Example response

JSON
{
  "engineUrl": "https://rtc.example.com",
  "region": "us-central1",
  "env": "dev",
  "apiId": "a_us-central1"
}

GET /v1/protocol

Which protocol versions this endpoint speaks, and when each one stops being accepted.

Who may call it: Anyone, with no credential.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

None.

Response body

FieldTypeDescription
versionsarray of ProtocolVersionOne row per protocol version the service still speaks.

Example response

JSON
{
  "versions": [
    {
      "version": 1,
      "status": "current"
    }
  ]
}

Benchmark

What the benchmark page measures a connection with. It opens no session, holds no credential and belongs to no project, and one address may start one run a minute.

POST /v1/bench/tickets

Start a benchmark run: one throwaway engine room and the two credentials a page measures its own connection with.

Who may call it: Anyone, with no credential.

Permission: none. The credential class above is the whole of what this route asks for.

Request body

None.

Response body

FieldTypeDescription
roomstringThe engine room this run is measured in. It is made for this answer, holds two participants at most, and finishes once it empties.
signalingUrlstringWhere both ends dial the engine, the same address GET /v1/whoami answers with.
senderBenchParticipantThe end that sends. Both ends are the same browser: this one publishes the run's frames.
receiverBenchParticipantThe end that receives, and where delivery is measured.
expiresInnumberSeconds both credentials are good for. It is the whole of how long one run may last, and there is no way to extend it: a longer run is a new one.

Example response

JSON
{
  "room": "bench-4f1c9a2b7e30",
  "signalingUrl": "wss://rtc.example.com",
  "sender": {
    "ticket": "eyJhbGciOi..."
  },
  "receiver": {
    "ticket": "eyJhbGciOi..."
  },
  "expiresIn": 60
}

The join code

A session answers JoinCodeMaterial, and turning that into the 15 characters a client scans is the one thing an SDK does locally. Over HTTP you do it yourself.

The code is <version><region><sessionCode><rotating>, all of it over A-Za-z0-9 and case-sensitive: the version digit, 1; the material's regionHint letter; the material's 7-character sessionCode; and a 6-character rotating part you compute. Show it exactly as built, never upper- or lower-cased. Any grouping you print it in is cosmetic, and a person may type it back with spaces or dashes.

The rotating part is RFC 4226 HOTP over a time counter, rendered base 62:

  1. counter = floor(unixMillis / 1000 / step), where step is the session's joinCodeRotation: 30s by default, 15s to 2m, and the counter uses its whole seconds.
  2. mac = HMAC-SHA256(secret, counter as 8 big-endian bytes), where secret is the material's secret decoded from base64 (32 bytes).
  3. Dynamic truncation, extended to five bytes: offset = mac[31] & 0x0f, then take the five bytes at offset as a big-endian number and keep its low 35 bits.
  4. Render that number base 62 over the digits 0-9A-Za-z (A is 10, a is 36), and left-pad it with 0 to 6 characters.

The service accepts the counter for now and one step either side, so a code a client sees stays accepted for at least one full step and at most two: 30 to 60 seconds at the default. Recompute it every step while the session is open.

Worked, with secret c2VjcmV0LWtleS1mb3ItdGhlLXdvcmtlZC1saW5lISE=, sessionCode 7F3a9Ce, regionHint C and Unix ms 1774000000000: the counter is 59133333, the rotating part is Qlexea, and the code is 1C7F3a9CeQlexea.

Where a client goes is the material's joinUrl with the code after the #.

Operator routes

These belong to the service running itself rather than to an application. They are listed here because they answer on the same base URL as everything above, so this page is the whole of what the service answers. Nothing an SDK does calls one.

Method and pathWho may call itWhat it does
GET /v1/project/deadlines/parkedAn organization key carrying project:read, or a console sign-in.The deadlines a sweep parked after they kept erroring, so what is stuck can be seen. Each entry carries the kind and member the unpark route takes.
POST /v1/project/deadlines/{kind}/{member}/unparkAn organization key carrying project:manage, or a console sign-in.Put a parked deadline back in the due set, so the next sweep takes it again. {kind} is one of reconnect, background, host_resume, idle, max_time, snapshot, rotation—the scheduler's own deadline kinds, and not the per-client kinds an SDK sees. {member} is what that kind is keyed by: a session id for the session-scoped kinds, <sessionId>:<clientId> for reconnect and background, and a key id for rotation, a key rotation whose retirement was already overdue when the retire sweep first read it. An unparked rotation is due at the moment of the unpark, not at its old time, and retires the old key on the next sweep. Both come from entry.kind and entry.member of the parked list, so nothing has to be composed by hand.
PUT /v1/releases/{version}/digestsAn organization key, or a console sign-in.Record the sha256 of every SDK download of one release, once. The release cut calls it. Platform staff only.
POST /v1/platform/noticesAn organization key, or a console sign-in.Mail a release notice or a planned interruption of the engine node to every owner of every live organization, once per release or window. The release tool calls it. Platform staff only.

Error codes

Every value code takes. A row with no status is raised by an SDK without a round trip, and a row whose status reads not returned is part of the vocabulary that no route answers with today: nothing to branch on.

CodeHTTPRetryableClearsMeaning
validation400noneverThe request or the session config is malformed; details.field names the field, details.header the header.
quota_above_ceiling400noneverA limit asked for on a key or a token is less restrictive than the level above it, or a pending raise approved while its value is still above the organization's plan (details.scope org, details.ceiling the plan's value); details.scope says which level bound it.
e2ee_required400noneverThe session requires end-to-end encryption and this join carries nothing to do it with.
invalid_key401noneverThe key is not usable, and details.reason says which of the five: absent (no Authorization: Bearer at all, or no X-Bitskiff-Key on a join route), malformed (a string carrying none of the credential prefixes), unknown (well-formed and this deployment holds no such key), revoked, expired.
invalid_token401noneverThe token is not usable; details.reason is expired, revoked or unknown, and the answer is a fresh token from your own backend.
handoff_invalid401noneverThe handoff ticket presented to POST /v1/handoff cannot be redeemed, or a resume names a place whose handoff it can no longer finish; details.reason is expired, spent, unknown, superseded, cancelled or lapsed, and the app shows one line then its scanner: a scan is the way in.
origin_not_allowed403noneverA publishable key without an allowed Origin.
restricted_mode403noneverA publishable-key open on a restricted project.
plan_expired403noneverThe organization's plan passed its expiry, so opening a session and joining one are refused until staff renew it; sessions already live run to their own end, and reads, keys and sign-in keep working.
secret_key_in_browser403noneverA secret or organization key presented with any Origin header; audited.
forbidden403noneverThe credential may not call this route, does not own this session, or lacks a permission it needs; details.reason says which.
wrong_client403noneverThis code is for a different client: the client key is not for this session, because the session is another project's or its host is one the key does not serve; details.reason is other_project or other_host and names nothing else; on POST /v1/handoff it is also other_app, a key of this project that is not the registered app's.
not_found404noneverNothing the caller can see holds the id, slug or path the request named; a route this service does not carry answers this way, with details.path.
session_not_found404noneverNo session with this id is live for this caller: none is, or the host or resume token presented is not a handle of it; it never says a session is live.
invalid_code404noneverThe join code did not decode, or its rotating part did not verify; counted against the bad-code limit.
clients_full409nowaitingThe session is at maxClients.
session_ended409noneverThe session has already ended; a fresh open is the way on.
host_released409nowaitingThe host has let its engine connection go and serves its clients over another transport, so nothing reaches it to tell it a client waits; the join may be tried again once the host rejoins.
key_rotating409nowaitingThe key is inside its rotation overlap and cannot take this change.
join_url_missing409noneverThe project has no joinUrl to point a client at; details.field is joinUrl.
replaced409noneverA resume after the engine's duplicate_identity close, and a later ticket was minted for this identity since: a second holder of the same token took its place, so this handle ends replaced.
handed_off409noneverA resume after the engine's duplicate_identity close, and the place's handoff committed to the successor this page named: the place lives on under it and this handle's part is over, so it ends handedOff.
handoff_pending409yeswaitingA resume after the engine's duplicate_identity close, and the place's successor has redeemed and not yet committed; ask again after retryAfterMs, which is not a budget, while the service reads the engine.
subject_mismatch403noneverOn POST /v1/handoff: the token beside the registered app's key names a subject other than the place's, so the app cannot take this place; the handoff fails at stage refused and the page keeps the place.
key_mismatch403noneverOn POST /v1/handoff into a place that encrypts: the token beside the registered app's key has no cnf, the redeem carries no key_thumbprint, or the cnf names another key than the one the app will present, so the app cannot take this place; refused before anything moves, the handoff fails at stage refused and the page keeps the place.
idempotency_in_flight409yeswaitingAn open, or a handoff redeem, with this Idempotency-Key is still running; retry after retryAfterMs for its answer.
seat_spent410noneverThis client's budget is spent; rejoin with a code.
host_resume_expired410noneverThe host's resume window is spent; open a fresh session.
protocol_unsupported426noneverThe service does not speak the protocol version this caller asked for; details.supported lists the ones it does.
rate_limited429yeswaitingOver a rate limit, per key, token, IP or session; retryAfterMs set.
concurrency_limit429yeswaitingA concurrentSessions or concurrentClients ceiling is full; details.scope says which level bound the call, key or project, and retryAfterMs is 10000 ms on every path that sends this code.
quota_exceeded429nowaitingA usage cap or the spend cap is spent; retryAfterMs is the time to reset.
turn_quota_exceeded429nowaitingThe project's relay byte cap (turn_bytes_per_month) is spent: the direct path's relay credential route mints no credential, and the host stays on the engine for that client; retryAfterMs is the time to the month's reset.
internal500nowaitingSomething inside the service failed; requestId is the handle support asks for.
relay_not_configured503noneverThis environment runs no Bitskiff relay, or its relay lists no turn: URL, so the direct path's relay credential route has nothing to mint; the host offers with its own STUN or stays on the engine.
unavailable503yeswaitingThe service or a dependency is down; retryAfterMs set.
overloaded503yeswaitingThe platform is at its admission ceiling; the open or join was refused and nothing live was touched; retryAfterMs set.
network-yeswaitingThe request never reached the service.
timeout-yeswaitingThe request or the handshake did not finish inside its budget.
protocol-noneverA response or an envelope did not match the pinned protocol.
engine-nowaitingThe transport or the engine SDK failed; cause carries its error, never rethrown.
disposed-noneverThe handle was disposed before the call could finish.
listener_error-noneverA listener the app bound with on threw; details.event names the event and cause carries the throw.
sdk_internal-noneverSomething inside this SDK failed and no request was refused; cause carries the error, never rethrown, and the SDK's log line at error names the step.
e2ee_unsupported-noneverThis client cannot do the end-to-end encryption the project requires; answered at inspect.

Objects

The objects the bodies above are made of, each written once.

AuditActor

Who made a change: a console user, a key, or the platform itself.

FieldTypeDescription
typestring"user" | "admin_key" | "secret_key" | "token" | "system".
idstringThe actor's id: a console user, a key, or the platform itself.

AuditEvent

One audited change: what was done, to what, by whom and when.

FieldTypeDescription
idstringThe row's own id.
atstring (64-bit integer)When it happened, Unix ms.
orgIdstringThe organization it happened in.
projectIdstringThe project it happened in, when it was a project's change.
actorAuditActorWho did it.
actionstringWhat was done, such as key.created.
targetAuditTargetWhat it was done to.
beforeobject, string keys to stringThe fields that changed, as they were.
afterobject, string keys to stringThe same fields, as they are now.
requestIdstringThe request that made the change: the handle support traces it by.
ipstringThe address the request came from.

AuditTarget

What a change was made to, by its kind and its id.

FieldTypeDescription
typestring"key" | "project" | "session".
idstringThe id of what was changed.

BenchParticipant

One end of a benchmark run.

FieldTypeDescription
ticketstringWhat this end dials the engine with. It may send and receive data, may publish no track, and is good for this room only.

BillingAccountView

A billing account: the payer an organization's invoices are addressed to.

FieldTypeDescription
billingAccountIdstringThe bill_ id.
orgIdstringThe organization it belongs to, as org_....
namestringThe organization's own word for it, 1-64 characters.
isDefaultbooleanTrue on the organization's default billing account, the one the monthly fee lands on; exactly one billing account of an organization is the default.
legalNamestringThe billed party's legal name, as an invoice prints it; absent until entered.
billingEmailstringThe address invoices are sent to; absent until entered.
addressstringThe billed party's postal address, free text; absent until entered.
taxIdstringThe billed party's tax identifier, free text; absent until entered.
createdAtstring (64-bit integer)Unix ms it was created.
projectIdsarray of stringThe proj_ ids of the projects attached to it now.

BillingMeterTotal

One owned meter summed over every project of the roll-up.

FieldTypeDescription
meterstringAs BillingUsageLine.meter.
legstringAs BillingUsageLine.leg.
quantitynumberThe sum of that meter's lines.
unitstringAs BillingUsageLine.unit.

BillingUsageLine

One project's owned meter over the month: the line an invoice will carry.

FieldTypeDescription
projectIdstringThe project, as proj_....
meterstringThe meter: client_minutes, session_minutes, relay_bytes or data_bytes_sent, the four billable legs.
legstringThe leg the meter is, as that table names it.
quantitynumberThe month's quantity over the days the project was attached here.
unitstringclient minutes, session minutes or bytes.

CapacityLimit

One cap that is not ok.

FieldTypeDescription
limitstringThe parameter spelling details.limit carries (clientMinutesPerDay).
scopestringWhat the cap is counted over: project, key, subject, ip, session, client, org or address. It is the same word a refusal carries in details.scope.
scopeIdstringThe same id a refusal and a limit.* webhook carry.
statusstringOne of CAPACITY_STATUS_OK, CAPACITY_STATUS_WARNING, CAPACITY_STATUS_EXCEEDED.
valuestring (64-bit integer)What the period has spent, in the row's own unit.
ceilingstring (64-bit integer)The cap it is spending against.
resetAtstring (64-bit integer)Unix ms when the period rolls; 0 for a cap with no period.

CapacityNotice

What a live session learns about its project's caps (the SDK's CapacityNotice).

FieldTypeDescription
statusstringThe worst row's; OK over an empty list. One of CAPACITY_STATUS_OK, CAPACITY_STATUS_WARNING, CAPACITY_STATUS_EXCEEDED.
limitsarray of CapacityLimitOne row per cap that is not OK; more than one stands at once.
atstring (64-bit integer)Unix ms on the service's own clock: when this standing was read.

ChannelRow

One row of a session's channel table.

FieldTypeDescription
channelIdnumberAllocated from 1 upward in declaration order, unique for the life of the session and never reused inside it.
namestringNon-empty, at most 64 UTF-8 bytes; checked at declaration.
deliverystringOne of DELIVERY_RELIABLE, DELIVERY_LOSSY.
orderedbooleanHanded over in send order; true on reliable, false on lossy by default.
lifetimeMsnumber0 = unset.
maxHznumberThe most messages a second one sender sends on this channel.
maxBytesnumberThe largest one message on this channel, sent or accepted.
latestOnlybooleanLossy only: drop a received packet behind the latest delivered.
linkMaxHznumberThe same row's rate on a direct link: the service fills it at the clamp from the typed maxHz, capped by the project's linkMessagesPerSecond row, so it is never below maxHz and equals it until that row is raised.

ClientEnvironment

What a client says about the program it runs in: stated by the client SDK, never by the developer, on every Hello and on the join, resume and redeem requests, so a join, a resume and a handoff's first Hello each restate it.

FieldTypeDescription
platformstringThe os-family registry: "android" | "ios" | "windows" | "macos" | "linux" | "other".
runtimestringThe client-runtime registry: "browser" | "webview" | "native".
sdkNamestringThe client SDK's package name, such as "ts-client": the first half of Hello.sdk.
sdkVersionstringThe client SDK's semver, such as "0.2.3": the second half of Hello.sdk, split so a roster can show the version alone.
transportstringThe transport this Hello arrived on: "server" (the session connection), "peer", or a declared transport's name.

ClientUsage

One client's usage, attributed on the client's own key and subject: what a developer that bills the people who took part bills on. The same fields and the same id on the ledger and the client.gone webhook.

FieldTypeDescription
idstringThe record's one identifier: the event id of the client.gone that closed it, or, for a client closed by its session's end, that session.ended event id joined with the client id. Dedupe on it.
sessionIdstringThe session the client was in.
clientIdstringThe client's id in that session.
keyIdstringThe client key the client joined with. Empty for the sample client page.
sessionKeyIdstringThe key that opened the session, the session record's keyId.
subjectstringThe subject of the client's own join token. Empty for an anonymous client.
labelstringThe host's label for the session.
joinedAtstring (64-bit integer)When the client joined, Unix ms.
goneAtstring (64-bit integer)When the client's place was released, Unix ms.
goneReasonstringWhy the client is gone, a gone reason.
bundlestringThe bundle the client held when it left. Price a promoted client on msByBundle, not on this.
seatMsstring (64-bit integer)The client's time in milliseconds: connected, away or disconnected. Joining is not counted.
msByBundleobject, string keys to string (64-bit integer)Time under each bundle the client held, joining included, so it sums to its whole time.
resumedbooleanWhether the client reconnected into its place inside its budget.
bytesReportedstring (64-bit integer)The client's bytes through the engine on every channel and direction, as the host reported them. A display figure, never a price.
writtenAtstring (64-bit integer)When the ledger last wrote this record, Unix ms. Absent on the webhook.
writerRevisionnumberWhich revision of the ledger's writer wrote the record. Absent on the webhook.
subjectUnknownbooleanTrue when the client's client record could not be read when it left, so subject and keyId are empty because they are unknown, not because the client joined without them.
messagesReportedstring (64-bit integer)The client's messages on every channel and direction, as the host reported them, beside bytesReported. A display figure: peer and declared transports are logged, never here.
dataMessagesUpstring (64-bit integer)Messages the service received from the client on the server transport, as the service counted them: the message figure to bill a subject on. Counted as each of the client's connections closes. Messages on peer or a declared transport never pass through the service and are not counted.
dataMessagesDownstring (64-bit integer)Messages the service sent to the client on the server transport, as the service counted them. Not counted on peer or a declared transport.
dataBytesUpstring (64-bit integer)Payload bytes of dataMessagesUp, as the service measured them: the byte figure to bill a subject on. Not counted on peer or a declared transport.
dataBytesDownstring (64-bit integer)Payload bytes of dataMessagesDown, as the service measured them. Not counted on peer or a declared transport. On the webhook, a connection still closing when the client left is not in these four yet; the ledger record has it.

ControlCount

One kind of control frame and how many of it arrived.

FieldTypeDescription
kindstringThe frame's kind, such as hello, presence, ping, heartbeat or transport_setup.
countstring (64-bit integer)How many frames of this kind arrived.

FixedCodeView

One fixed code of the project: a join code that does not rotate, for an NFC tag or a printed QR.

FieldTypeDescription
idstringThe opaque id a host names at open; it admits nobody.
labelstringThe developer's own text for the NFC tag or sticker that carries it.
codestringThe 15-character code, filled on the create and replace answers only and empty on every other: the service keeps only its SHA-256, so a lost code is replaced.
sessionIdstringThe open session holding the claim; empty when none does.
createdAtMsstring (64-bit integer)Unix ms.
replacedAtMsstring (64-bit integer)Unix ms of the last replace; 0 for never replaced.

HandoffHint

The page's transport as the page stated it on the mint: a fact about a class, never an instance.

FieldTypeDescription
transportstringThe class: "peer", "relay", "server" or a declared transport's name.
gradestringIts last grade, "good", "fair" or "poor"; empty when none was read.
rttMsnumberIts last round trip time reading, in ms; 0 when none was read.
noDirectPathbooleanThe page found no direct path from where it stood.

HandoffSuccessor

The successor of a handoff under way, as a host reads it.

FieldTypeDescription
kindstringThe successor's kind of process: "page" | "app".
phasestringOne of HANDOFF_PHASE_PENDING, HANDOFF_PHASE_REDEEMED, HANDOFF_PHASE_DONE, HANDOFF_PHASE_FAILED.
deadlineMsstring (64-bit integer)Unix ms on the service's clock the handoff window ends.
hintHandoffHintThe page's transport as it stated it on the mint; absent when it stated none.

InvitationView

One invitation into an organization: who was invited, what accepting it writes, and where it stands.

FieldTypeDescription
invitationIdstringThe invitation's id.
orgIdstringThe organization the invitation is into.
emailstringThe address that was invited.
rolesarray of stringThe roles accepting it writes.
projectIdstringEmpty unless a role is a project role.
invitedBystringThe user_ id of the console user who sent it.
createdAtstring (64-bit integer)Unix ms.
expiresAtstring (64-bit integer)Unix ms.
acceptedAtstring (64-bit integer)Unix ms; 0 while it is not accepted.
revokedAtstring (64-bit integer)Unix ms; 0 while it is not revoked.
statestring"open", "accepted", "revoked" or "expired", resolved against the clock at read time so a caller does not have to hold one to know which row is still actionable.
kindstring"organization" (into an existing organization) or "platform" (creates a new one on acceptance).
planPlanRequestThe plan the organization created on acceptance will carry. Set only on a platform invitation.

InvoiceLineView

One line item of an invoice.

FieldTypeDescription
lineNonumber1-based, in the order the invoice prints its lines.
kindstringusage (one project and one owned meter over the month), fee (the monthly fee parameter, on the organization's default billing account), credit or adjustment.
projectIdstringThe project a usage line is for, as proj_...; empty on the fee line.
meterstringclient_minutes, session_minutes, relay_bytes or data_bytes_sent on a usage line; fee_monthly on the fee line.
legstringWhat the line is, as and 11 name it.
quantitynumberThe month's quantity over the days the project was attached here.
unitstringclient minutes, session minutes, bytes or month.
unitPriceMicrosstring (64-bit integer)The line's rate card parameter, the price per unit in millionths of currency; every owned meter and the monthly fee has its own.
amountMicrosstring (64-bit integer)The line's amount, in millionths of currency, rounded to the cent.
reasonstringWhy a credit or an adjustment line exists; empty otherwise.

InvoiceView

An invoice: one per billing account per calendar month (UTC), numbered in the billing account's own sequence once issued and never edited after.

FieldTypeDescription
invoiceIdstringThe invc_ id.
billingAccountIdstringThe billing account it is addressed to.
orgIdstringThe billed organization.
periodstringThe UTC month it covers, as YYYY-MM.
periodStartstring (64-bit integer)Unix ms the month starts, inclusive.
periodEndstring (64-bit integer)Unix ms the next month starts, exclusive.
statusstringissued, paid or void; a zero total is issued already paid, and a void keeps its number.
numberstringThe printed number: eight characters of the billing account's id, then its own sequence, as Q2XM9PBZ-0001; the numbers of one billing account have no gap.
rateCardVersionnumberThe version of the rate card it was rated on.
currencystringISO 4217, as USD.
subtotalMicrosstring (64-bit integer)The sum of the lines, in millionths of currency.
totalMicrosstring (64-bit integer)What is owed, in millionths of currency.
linesarray of InvoiceLineViewEvery line item on a read of one invoice; empty on a list, which carries headers only.
accountNamestringThe billing account's name when it was issued.
legalNamestringThe billed party's legal name when it was issued; absent when none was entered.
billingEmailstringThe address invoices were sent to when it was issued; absent when none was entered.
addressstringThe billed party's postal address when it was issued; absent when none was entered.
taxIdstringThe billed party's tax identifier when it was issued; absent when none was entered.
issuedAtstring (64-bit integer)Unix ms it was issued.
paidAtstring (64-bit integer)Unix ms it was marked paid; 0 while it is not.
voidedAtstring (64-bit integer)Unix ms it was voided; 0 while it is not.
statusReasonstringWhy it is paid or void: zero_total on an invoice issued already paid, else the reason staff gave; empty while it is issued.
pdfSha256stringThe hex SHA-256 of the archived PDF.
frozenThroughstring (64-bit integer)How far the usage rollups had frozen the month when its lines were read, Unix ms; 0 when nothing was frozen.

JoinCodeMaterial

What the host needs to render and rotate the join code locally.

FieldTypeDescription
versionstring"1".
regionHintstringOne char.
sessionCodestring7 chars; the host builds the 15-character join code around it.
secretstring (base64)32 bytes; HMAC key for the TOTP component.
stepSnumberConfig.joinCodeRotation, in the whole seconds the counter uses.
joinUrlstringThe project's joinUrl with "#" appended; the SDK appends the code.

JoinGrant

A join grant: opaque, session-bound, single-use, and holding no place.

FieldTypeDescription
grantstringThe grant itself: base64url, unpadded, of 32 random bytes.
expiresAtMsstring (64-bit integer)Absolute Unix ms on the service's clock past which the grant no longer redeems.
serverTimeMsstring (64-bit integer)The service's own Unix ms at the moment it answered, the anchor expiresAtMs is read against.

JoinGrantEntry

The grant half of JoinRequest.entry: the grant and the holder secret, which arrive together or not at all.

FieldTypeDescription
grantstringAs JoinGrant.grant.
holderstringBase64url, unpadded, of the 32-byte holder secret whose hash the reservation carried.

JoinReserve

What a page asks a join grant for: the hash of the holder secret it keeps and will present when it redeems.

FieldTypeDescription
holderHashstringBase64url, unpadded, of the SHA-256 of the 32-byte holder secret the page generated.

KeyLimits

A key's limits{}: every limit is spelled in camelCase, the spelling a refusal carries in details.limit.

FieldTypeDescription
concurrentSessionsnumberHow many sessions this key may hold open at once.
concurrentClientsnumberHow many clients this key's sessions may hold at once.
sessionOpensPerMinutenumberHow many sessions this key may open in a minute.
clientMinutesPerDaynumberHow many client minutes a day this key may spend.
ticketMintsPerMinutenumberHow many tickets this key may mint in a minute.
tokensMaxTtlstringThe ceiling on a mint's ttl and on a live token's effective expiry, clamped min.
requireTokenstringThe ordered enum "off" < "token" < "token_bound_to_session": a key may require, never relax, and what it requires reaches that key's sessions only.
joinsPerMinuteKeynumberjoinsPerMinuteKey: joins one client key admits in a minute, across every session and address it joins from.
bytesPerSecondnumberHow many bytes a second each client joining with this key may send in each direction. It may sit under the project's value and never above it.
messagesPerSecondnumberHow many messages a second each client joining with this key may send. It may sit under the project's value and never above it.
linkMessagesPerSecondnumberThe link ceiling, per client of the key: may sit under the project's and never under this key's own messagesPerSecond.

KeyView

A key, masked, or once with its full string.

FieldTypeDescription
keyIdstringThe key's id, which is what every route names it by. It is not the key string.
typestring"publishable" | "secret" | "admin".
orgIdstringThe organization the key belongs to.
projectIdstringPublishable and secret keys only.
projectsarray of stringOrganization keys only; empty means every project in the organization.
maskedstringThe key as a listing may show it. A secret or organization key is truncated, bsk_sk_a7k2m9xq...jy2k; a publishable key is the one class stored and listed in clear, so its masked is the whole key.
secretstringThe one-time full string; empty otherwise.
statusstring"active" | "rotating" | "revoked".
labelstringYour own name for the key.
originsarray of stringThe origins a publishable key may be used from.
limitsKeyLimitsThe caps this key is held to.
createdAtstring (64-bit integer)When the key was created, Unix ms.
createdBystringWho created it: the console user, or the key that minted it.
lastUsedAtstring (64-bit integer)When the key was last presented, Unix ms. Zero on a key that has never been used.
retireAtstring (64-bit integer)When a rotating key stops being accepted, Unix ms. Zero on a key that is not rotating.
successorKeyIdstringThe key that replaced this one in a rotation.
revokedAtstring (64-bit integer)When the key was revoked, Unix ms. Zero while it is live.
revokedReasonstring"manual" | "rotation" | "compromise".
permissionsarray of stringThe permission list the key holds: ["sessions:open"] on a host key, ["sessions:join"] on a client key.
expiresAtstring (64-bit integer)When the key expires, Unix ms; 0 on a key that carries no expiry (optional on every class, capped at one year, 90 days by default on an organization key; the login key has none).
ancestryarray of stringThe keys that made this one, root first, ending with its immediate maker.
hostKeysarray of stringA client key's conditions.hostKeys: the host keys whose sessions it may join.
subjectsarray of stringA key holding tokens:mint: its conditions.subjects, the subject prefixes a mint under it may name.
approvedBystringOn the login key, the organization key bitskiff login and bitskiff init make: the console user who approved the sign-in.

LimitQuota

One cap, as it applies to the credential that asked.

FieldTypeDescription
valuestring (64-bit integer)The cap itself.
scopestringWhich level set the value: project, key, or org when the organization's plan holds it under the project's own, so a plan change and not a project raise is what lifts it.
usedstring (64-bit integer)The current period's consumption; 0 for a rate limit, which has no period to spend.
periodEndstring (64-bit integer)Ms since epoch when used resets; 0 for a rate limit.

LimitRaiseView

One pending limit raise: a project asked for a value above its organization's plan ceiling (POST /v1/project/limits/raise), and the current value applies until an organization owner approves or denies it.

FieldTypeDescription
projectIdstringThe project that asked, as proj_....
projectNamestringThat project's name, for a person reading the list.
limitstringThe limit in its parameter spelling (concurrentSessions), as a refusal's details.limit names it.
valuestring (64-bit integer)The value asked for.
currentstring (64-bit integer)The project's own value, which applies while the raise is pending.
ceilingstring (64-bit integer)The plan's ceiling for the limit now; absent when the plan no longer bounds it.

LostNode

The custom engine process a client's link was on when the engine said it is gone or going: the node and boot of the bye that ended the link, copied as they arrived.

FieldTypeDescription
nodestringThe node's name, as the bye named it.
bootstringThe boot id of the process on that node, as the bye named it.

MemberView

One member of an organization, as GET /v1/orgs/{orgId}/members lists them and the PATCH and DELETE on one member answer them.

FieldTypeDescription
userIdstringThe user_ id of the console user.
emailstringThe verified address the console user signs in with; empty when the store holds no record of them.
rolestringThe organization role the relations amount to, highest first: org_owner, org_admin, billing_admin or org_member.
relationsarray of stringThe organization relations held directly, from owner, admin, billing_admin and member; a relation computed from another (an owner is an admin) is not repeated here.
joinedAtstring (64-bit integer)Unix ms the first of those relations was written; 0 when the store kept no time.
callerbooleanTrue on the row of the console user making the request, whose own role they may not change.

OrgPlan

The plan on an organization's record.

FieldTypeDescription
namestringThe plan's name, such as experimental or unlimited.
ceilingsobject, string keys to string (64-bit integer)The plan's values, keyed by the catalogue parameter (concurrentSessions). A row absent here has no plan bound.
expiresAtstring (64-bit integer)Unix ms; absent means it does not expire.
setAtstring (64-bit integer)Unix ms.
setBystringWho wrote it: a user_ id, a service account, or system.

OrgView

One organization, as GET /v1/orgs and POST /v1/orgs answer it. It carries nothing about the caller: which relation they hold in it is what GET /v1/auth/principal answers.

FieldTypeDescription
orgIdstringThe organization's id.
namestringThe organization's name.
createdAtstring (64-bit integer)Unix ms.

OrganizationMembership

One organization the user holds a relation in, with the highest one they hold there.

FieldTypeDescription
orgIdstringThe organization's id.
namestringThe organization's name.
relationstringThe highest of "owner", "admin", "member".

ParticipantHolder

The process that keeps a place's resume token.

FieldTypeDescription
kindstringThe kind of process that keeps the place: "page" | "app".
nextHandoffSuccessorThe successor a handoff named, while that handoff is under way; absent otherwise.

ParticipantMeters

One client's own meters, as the service has them: which client it is, and its counts.

FieldTypeDescription
participantIdstringThe client these meters are for, by its id on this wire.
countersobject, string keys to stringThat client's counters by meter name. Each count is a string, because these are 64-bit numbers.

ParticipantStats

One client's own entry in a stats report: which client it is, and what it counted.

FieldTypeDescription
participantIdstringThe client these counters are for, by its id on this wire.
statsStatsWhat that client reported over the window.

ParticipantView

One client of a session, as the service holds it: what it may do, the place it holds, and where it is.

FieldTypeDescription
participantIdstringThe client's id on this wire.
grantsarray of stringWhat this client may do: send input, receive input, broadcast, receive broadcasts, manage. One of GRANT_SEND_INPUT, GRANT_RECEIVE_INPUT, GRANT_BROADCAST, GRANT_RECEIVE_BROADCAST, GRANT_MANAGE.
openerbooleanTrue for the host, which holds the place the session was opened on.
seatClassstringWhich kind of place this client holds, "controller" | "viewer" | "opener". It follows the grants the client actually holds, so a client whose grants change changes class with them.
seatnumberWhich place of that class this client holds, counted from one. It is stable across a resume, so the second controller to join is 2 for as long as it holds its place.
statestringWhere the client is: one of joining, connected, reconnecting, released, away or gone.
presencestringWhether the client is in front of the person using it or backgrounded. One of PRESENCE_PRESENT, PRESENCE_AWAY.
joinedAtstring (64-bit integer)When the client joined, Unix ms.
sdkstringWhich SDK the client joined with and its version, such as ts-client/0.1.0.
transportPathstringThe path this client last reported in its stats: "udp", "tcp", "turn" or "unknown". It is "unknown" until the client reports one, and stays "unknown" for a client whose runtime cannot see which pair it is on.
bundlestringThe bundle this client joined with. One of BUNDLE_CONTROLLER, BUNDLE_VIEWER, BUNDLE_HOST.
subjectstringThe subject of the join token this client presented: the developer's own user id, straight off this client's own record.
goneReasonstringWhy this client is gone: a gone-surface reason of the reasons registry, straight off the client record.
limitsSessionLimitsThis client's own traffic caps: min(project row, the client key's own row), computed at join.
connectionstringThe record's connection, "reserved" | "attached" | "detached" | "released" | "gone".
keyIdstringThe id of the client key that admitted this client (key_...), never the key itself.
originstringThe origin this client was admitted from: the Origin header a browser sent, or the origin a native or app-shell client declared where the header was absent or null; for the keyless sample client page, that page's own origin.
originSourcestringHow origin arrived: "header" for the Origin header a browser sent, "declared" for the origin a native or app-shell client named in its request body.
releasedTransportstringThe transport that carries a released client (connection "released"), as its host named it at the release; empty for every other connection.
holderParticipantHolderWho keeps this place: the holder's kind and, while a handoff is under way, its successor.
entrystringHow this client's place was entered, one of these values: "code" (a rotating join code) or "fixed".
keyThumbprintstringThe cnf.jkt the developer's backend bound into this client's join token at the mint: the RFC 7638 thumbprint of the key the client must present.
grantsCeilingarray of stringThe grants ceiling of the join token that admitted this client: the most its grants can ever be, since admission and every grant change are clamped to it. One of GRANT_SEND_INPUT, GRANT_RECEIVE_INPUT, GRANT_BROADCAST, GRANT_RECEIVE_BROADCAST, GRANT_MANAGE.
environmentClientEnvironmentThe environment this client stated on its latest join, resume or committed handoff, as the service checked it against the os-family and client-runtime registries.
metadataobject, string keys to stringThe developer's own metadata for this client, as that same request passed it, inside the client metadata bounds.

PlanRequest

A plan as staff write it: a named template (experimental, unlimited), resolved into values when it is written, with single rows overridden.

FieldTypeDescription
namestringThe named plan to set. Its template resolves into values on the organization's record.
ceilingsobject, string keys to string (64-bit integer)Overrides on the template, keyed by the catalogue parameter (concurrentSessions), the one spelling on every wire surface; only rows a project may raise.
expiresAtstring (64-bit integer)Unix ms.

PlatformCostLeg

One cost leg for one organization's month.

FieldTypeDescription
legstringThe leg's name as writes it.
quantitynumberThe month's quantity summed over the organization's projects, in unit.
unitstringThe unit of quantity: a minute leg's minutes, GB, messages or requests.
quantityKindstringWhat the quantity is: measured, client reported, an estimate, or not metered per project.
unitCostnumberWhat one unit costs Bitskiff in dollars.
unitCostKindstringWhat the unit cost is: an assumption, a parameter, free to Bitskiff, or none stated.
costnumberThe product of quantity and unit cost, in dollars.
costKindstringWhat the cost is: an estimate where a byte leg is priced at the egress price, an assumption where a minute leg is, free to Bitskiff, or empty when there is no cost.
inTotalbooleanWhether cost is in the response's total.
notestringWhy the leg reads as it does, in one line; empty when nothing needs saying.

PlatformOrgView

One organization on the staff list: its plan record, who owns it, and what it is running now.

FieldTypeDescription
orgIdstringThe organization, as org_....
namestringThe organization's name.
createdAtstring (64-bit integer)Unix ms.
ownerEmailsarray of stringThe address of every organization owner, sorted.
planOrgPlanThe plan record as it stands.
expiredbooleanWhether the plan has passed its expiry at answer time.
projectCountnumberHow many projects the organization holds.
liveSessionsnumberLive sessions across its projects, read from the gates.
liveClientsnumberClients holding a place in those sessions, from the gates.
clientSlotsnumberThe client slots the plan commits: its concurrentClients ceiling, counted at organization grain.
platformSlotsnumberThe platform ceiling's slots: platformConcurrentParticipants, hosts and clients together.

PlatformSdkRelease

One release on the platform list, with the organizations that ran it in the last 30 days.

FieldTypeDescription
releaseSdkRowThe release, its sessions counted across every organization.
orgIdsarray of stringThe organizations that ran it, ordered by id.
refusedOrgsnumberOrganizations that ran it and refused staff access: counted here, never named in orgIds.
orgsarray of PlatformSdkReleaseOrgThe organizations orgIds names, each marked when it is the platform owner's.

PlatformSdkReleaseOrg

One organization that ran a release, as the platform list names it.

FieldTypeDescription
orgIdstringThe organization's id.
ownerbooleanTrue for the platform owner's organization (the bootstrap organization), whose apps are never live apps.

ProjectEvent

One event of the project's stream, the same object the webhook delivers to your URL.

FieldTypeDescription
idstringThe event's own id, which is what a webhook delivery is deduplicated on and what this stream is paged by.
typestringWhich kind of event this is, such as open.refused.
atstring (64-bit integer)When the event happened, Unix ms on the service's clock.
datastringThe event's own fields, JSON-encoded: the same bytes the webhook delivery carries under data.

ProjectMembership

One project the user holds a relation on, with every project relation they hold there.

FieldTypeDescription
projectIdstringThe project's id.
slugstringThe project's slug, as the console's address names it.
orgIdstringThe organization the project is in.
relationsarray of stringEach project relation this person holds there, of admin, key_manager, editor and viewer. editor brings viewer with it, and admin brings all four.

ProjectView

One project: how it is named and addressed, and the settings every session it opens inherits.

FieldTypeDescription
projectIdstringThe project's internal key. The slug is the handle you type.
orgIdstringThe organization the project belongs to.
restrictedModebooleanTrue when this project takes no publishable-key opens: a session must be opened with a secret key or a token.
joinUrlstringThe page a join code points a client at. Without one, an open is refused join_url_missing.
e2eestring"off" | "required".
namestringThe caller's own word for the project, 1-64 characters, not unique.
createdAtstring (64-bit integer)When the project was created, Unix ms.
slugstringThe project's public handle: globally unique, immutable, lowercase letters, digits and hyphens, 5 to 30 characters.
archivedAtstring (64-bit integer)When the project was archived, Unix ms; absent on a project that is not. An owner can unarchive it for 30 days after this.

ProtocolRange

The span of protocol versions one end can speak, oldest to newest.

FieldTypeDescription
minnumberThe oldest protocol version the caller can speak.
maxnumberThe newest protocol version the caller can speak.

ProtocolVersion

One protocol version the service still speaks, and when it stops.

FieldTypeDescription
versionnumberThe protocol version.
statusstring"current" | "supported" | "sunset".
sunsetstring (64-bit integer)When the service stops speaking it, Unix ms. Zero while no date is set.

RegisteredApp

A native app a project registered for one platform: the only app an offer may name, and the only client key that may redeem a handoff ticket.

FieldTypeDescription
platformstringandroid or ios.
appIdstringThe Android package name, or the iOS bundle id.
fingerprintsarray of stringAndroid: the SHA-256 signing certificate fingerprints, colon-separated upper-case hex, as assetlinks.json spells them.
storeIdstringIOS only: the numeric App Store id the listing is built from.
handoffLinkstringThe https handoff link, on a host the app claims (assetlinks.json, apple-app-site-association) and the page is not served from.
clientKeyIdstringThe id (key_) of the publishable client key the app joins with.
namestringThe app's name as the offer shows it: "Continue in the {name} app".
gainstringWhat the app gives, in the developer's words, at most 40 characters: the offer's one line.

RegisteredAppListPatch

A presence-carrying wrapper for the registered app list, as TerminationRuleListPatch is for rules: absent leaves the list alone, present replaces it whole, present with no values clears it.

FieldTypeDescription
valuesarray of RegisteredAppThe registered apps, one per platform; empty clears the list.

ReleaseDigest

One SDK tarball of a release: the download it names and the sha256 of its bytes.

FieldTypeDescription
packagestringThe package name, such as @bitskiff/host.
filestringThe download's path under /downloads/, such as 0.2.2/bitskiff-host.tgz.
sha256stringThe lowercase hex sha256 of the tarball's bytes.
bytesstring (64-bit integer)The tarball's size in bytes.

RelocatedClient

One client a re-placement is still waiting for.

FieldTypeDescription
participantIdstringThe client's client id.
releasesAtMsstring (64-bit integer)Absolute Unix ms its relocate deadline releases its place at.

Relocation

The answer to a host-ticket that re-placed its session, or found it already re-placed.

FieldTypeDescription
relocatedAtMsstring (64-bit integer)Absolute Unix ms on the service's clock the session was last re-placed at (relocatedAt).
clientsarray of RelocatedClientEach client not yet on the new node, with the instant its relocate deadline releases its place: the held time the host shows for a client it reports disconnected.

ReservationRefused

A refused join-grant reservation: the refusal a route would have answered, carried beside an inspect that still answers.

FieldTypeDescription
codestringrate_limited today: one of the two outstanding-grant ceilings is full.
retryAfterMsnumberHow long to wait before reserving again: until the soonest outstanding grant expires.
detailsobject, string keys to stringlimit (joinGrantsOutstanding or joinGrantsOutstandingSession), scope, value and ceiling, as on every limit refusal.

RttSummary

Round trips over one window, summarised: the shape of the window, and the totals since the client joined.

FieldTypeDescription
windowCountnumberSamples behind the window figures.
windowP50MsnumberMedian round trip over the window, in milliseconds.
windowP95Msnumber95th-percentile round trip over the window, in milliseconds.
windowMaxMsnumberThe slowest round trip in the window, in milliseconds.
totalCountstring (64-bit integer)How many round trips have been measured since this client joined.
totalSumMsstring (64-bit integer)Those round trips added together, in milliseconds, so an average over the whole client can be taken.

SamplePageSessions

The sample page's lifetime count for one project. One session is counted each time the project opens one while its joinUrl is the sample page, and nothing resets the count.

FieldTypeDescription
usedstring (64-bit integer)Sessions counted for the life of the project.
onboardingBoundnumberPast this count every read of the project's settings warns, and nothing is refused—100.
refusalBoundnumberPast this count a join through the sample page is refused until joinUrl names a page of the project's own—1000.
onSamplePagebooleanWhether the project's joinUrl is the sample page now. A project that moved to a page of its own keeps its count, and nothing more is counted.

SdkRow

One SDK release as the inventory saw it in the last 30 days: a package and version, from X-Bitskiff-SDK on a ticket mint, and the protocol version that mint negotiated.

FieldTypeDescription
sdkstringThe whole tag, <package>/<semver>, e.g.
protocolnumberThe protocol version the mints pinned.
lastSeenstring (64-bit integer)Unix ms of the latest mint that carried this release.
sessions30dstring (64-bit integer)Distinct sessions this release minted for in the last 30 days.
packagestringThe <package> half of the tag, e.g.
versionstringThe <semver> half, e.g.
protocolStatusstringThat version's status in GET /v1/protocol: "current" | "supported" | "sunset".
sunsetstring (64-bit integer)Unix ms that version is refused from; 0 while it has no sunset date.

SessionConfig

Requested at open, clamped by the service against project ceilings, echoed everywhere.

FieldTypeDescription
maxClientsnumberCapacity is one number and there are no per-role ceilings: a session holds this many clients at once and the next join is refused clients_full.
reconnectBudgetstringHow long a client may spend reconnecting before its place is given up, as a duration string: 15s, 2m.
backgroundBudgetstringHow long a client may spend backgrounded before its place is given up, as a duration string: 5m, 30m.
maxSessionTimestringHow long the session may run before the service ends it, as a duration string. 0s, the default, is no limit.
idleTimeoutstring0s = unlimited: the idle limit is off unless asked for.
joinCodeRotationstringHow often the join code rotates, as a duration string: 30s, 2m. It rotates on a whole number of seconds, so a step that is not one is refused.
hostResumeWindowstringThe host grace: how long the service keeps the session open for a host that dropped, as a duration string. At open no host has arrived to drop, so the session waits out the opener's own ticket instead—60 s—whatever you send here, which means a session opened ahead of its host holds a concurrency slot for that minute. From the first host attach on, this is the whole grace, and 0s ends the session the moment the host is gone.
warningLeadstringWarning lead: how long before a terminal deadline each SDK raises its own warning.
defaultGrantsarray of stringThe grants a new client starts with. One of GRANT_SEND_INPUT, GRANT_RECEIVE_INPUT, GRANT_BROADCAST, GRANT_RECEIVE_BROADCAST, GRANT_MANAGE.
terminationRulesarray of TerminationRuleThe session's termination policy: at most 16 rules, and the empty list, which is the default, is the platform's two budgets and nothing else.

SessionDeadlines

Every session-scoped deadline the service has armed, as absolute Unix ms on the service's clock; 0 is not a deadline in the past, it means this one is not armed.

FieldTypeDescription
idleDeadlineMsstring (64-bit integer)The idle limit: no client attached since this was armed, and at this instant the session ends idleTimeout.
maxTimeDeadlineMsstring (64-bit integer)maxSessionTime measured from open.
codeRotatesAtMsstring (64-bit integer)The next join-code step boundary.
holdReleaseAtMsstring (64-bit integer)A hold taken with a release instant; 0 for a hold that stands until somebody releases it.
releasedCeilingDeadlineMsstring (64-bit integer)The released-session bound: while the host is released, at this instant the session ends released_session_ceiling.

SessionDefaults

The session-config fields a project may give a default: the numeric rows of the session-config registry, spelled the way SessionConfig spells them.

FieldTypeDescription
maxClientsnumberHow many clients a session holds at once; 0 in a patch leaves the default alone.
reconnectBudgetstringDuration string; the empty string in a patch leaves the default alone.
backgroundBudgetstringDuration string; the empty string in a patch leaves the default alone.
hostResumeWindowstringHost grace, a duration string; 0s ends a session with its host.
idleTimeoutstringThe idle limit, a duration string; 0s is unlimited.
maxSessionTimestringA duration string; 0s is no maximum.
joinCodeRotationstringA duration string of whole seconds.
warningLeadstringA duration string; 0s asks for no warning.

SessionLimits

The limits a session hands its clients, so an SDK learns the cap it has to self-enforce from a response it already receives.

FieldTypeDescription
bytesPerSecondnumberThe bytesPerSecond row, per client per direction.
messagesPerSecondnumberThe messagesPerSecond row's bitskiff.in rate, per client: 120 unless the project lowered it.
linkMessagesPerSecondnumberThe linkMessagesPerSecond row, per client: the link ceiling, the class rate a client's lossy channels run at while its direct link is up.

SessionUsage

One session's usage: what a developer bills its own customers on. The same fields and the same id on the ledger, the session.ended webhook and the session view. Durations are milliseconds and instants Unix ms; how to round them is your own choice.

FieldTypeDescription
idstringThe record's one identifier: the event id of the session.ended that finalized it, the same on the ledger, the webhook, the view and the CSV, so dedupe on it. A day record's is {sessionId}:{YYYY-MM-DD}. Empty while the session runs: never bill a record without one.
sessionIdstringThe session.
projectIdstringThe project the session belongs to.
regionstringThe region the session ran in.
keyIdstringThe key that opened the session, or minted the token that did.
subjectstringThe subject the session was opened under: your own id for the customer the host ran for. Empty when no token opened it.
labelstringThe host's label for the session, as it ended.
metadataobject, string keys to stringThe session's own keys and values, as it ended.
openedAtstring (64-bit integer)When the session was opened, Unix ms.
endedAtstring (64-bit integer)When the session ended, Unix ms. On a day record it is set only on the day the session ended; absent while it runs.
endedReasonstringWhy the session ended, a session end reason. Empty while it runs, and on a session whose end never reached the service, which the ledger closes at its last known state.
attachedMsstring (64-bit integer)Time with a host attached, in milliseconds.
detachedMsstring (64-bit integer)Time in host grace or with no host at all, in milliseconds.
seatMsstring (64-bit integer)Client time in milliseconds: every client's time connected, away or disconnected, summed over every bundle. Joining is not counted.
seatMsByBundleobject, string keys to string (64-bit integer)The same client time, one entry per bundle name, for a price that differs by bundle.
clientsnumberClients admitted: one per accepted join, so a client that returned after it was gone counts twice. Dedupe people on the subject of the client records.
peakClientsnumberThe most clients held at once.
bytesReportedstring (64-bit integer)Bytes through the engine on every channel, direction and path, as the host reported them. A display figure, never a price. Bytes on peer or a declared transport are logged, never here.
dataBytesDownstring (64-bit integer)Message bytes the service sent to the session's host and clients, as the service measured them: the byte figure to price on. A day record carries that day's; a session record the sum of its days. 0 on the webhook and the view.
dataBytesUpstring (64-bit integer)Message bytes the service received from the session's host and clients, as the service measured them. A day record carries that day's; a session record the sum of its days. 0 on the webhook and the view.
dataBytesGradestringserver_measured when the service measured message bytes for the session. Empty, with both byte fields 0, when it measured none: always on the webhook and the view, which close before the measurement lands, and on sessions before the measurement began.
writtenAtstring (64-bit integer)When the ledger last wrote this record, Unix ms. Absent on the webhook and the view. A record that changed after you billed it carries a newer one.
writerRevisionnumberWhich revision of the ledger's writer wrote the record. Absent on the webhook, the view and a day record.
daystring (64-bit integer)A day record only: the UTC day it covers, as Unix ms of that day's 00:00. Every quantity of a day record is that day's share.
completebooleanA day record only: false when the day's last two minutes held no snapshot of the session, so the day's share may be short by that much.

SessionView

One session, as the service holds it: its config as it was clamped, its state, and what it has running.

FieldTypeDescription
sessionIdstringThe session's id.
projectIdstringThe project the session belongs to. Filled for the developer only (a secret key, an organization key or a console session); empty to a page (protocol.md 9.9).
configSessionConfigClamped.
ceilingsSessionConfigWhat the project may request.
labelstringYour own name for the session: what it was opened with.
metadataobject, string keys to stringYour own keys and values, as the session was opened with.
regionstringThe region the session runs in.
hostEpochnumberCounts up each time a host attaches to this session. A message carrying a lower epoch than the one already seen is a late copy, and is dropped.
statestringWhere the session is: one of open, host_attached, host_away, host_released or ended.
openedAtstring (64-bit integer)When the session was opened, Unix ms.
endedAtstring (64-bit integer)0 while live.
endedReasonstringWhy the session ended, and absent while it runs. One of host_closed, api_closed, idle_timeout, max_session_time, released_session_ceiling, host_resume_expired or lost—the same session end reasons the SDKs and the close codes carry: see Session end reasons.
participantsarray of ParticipantViewThe clients the session holds. Present on a read of one session and on close, and left out of the session list, which answers many sessions at once.
deadlinesSessionDeadlines16 is left for the hold member the session hold routes add.
serverTimeMsstring (64-bit integer)The service's own Unix ms at the moment it answered.
e2eebooleanWhether this session is end-to-end encrypted: its own mode, fixed when it opened and never changed after. POST /v1/join/inspect answers the same value before a client exists.
controllersHeldnumberHow many of the clients held at that moment hold the input grant, counted off the session's own record. Every answer computes it, the session list included, so the console's clients-in-use column and bitskiff sessions list have the number without reading a roster; the participants list itself comes only from a read of one session. Like every scalar it is left out of the JSON at zero, so read an absent field as none rather than as unknown.
viewersHeldnumberHow many of the clients held at that moment hold no input grant.
subjectstringThe subject the session was opened under: the user id your backend minted the opening token for. Empty when no token opened it.
keyIdstringThe id of the key that opened the session, or minted the token that did. Filled for the developer only (a secret key, an organization key or a console session); empty to a page.
usageSessionUsageThe session's usage record, on a read of one session only. While the session runs it is a running total with an empty id, never a figure to bill; once it has ended it is the final record, with the same id the session.ended webhook and the ledger carry, for as long as the session is kept.
messagesToHoststring (64-bit integer)How many messages the session's clients have sent its host, control frames left out, as the host reported them. On a read of one session only. With a client in participants and a message each way, the session works end to end.
messagesToClientstring (64-bit integer)How many messages the session's host has sent its clients, control frames left out, as the host reported them. On a read of one session only.

SignInProvider

One federated provider and whether it is on.

FieldTypeDescription
namestringThe provider: google or microsoft, as in /v1/auth/sign-in/oidc/{provider}.
enabledbooleanTrue when this deployment holds a client id for the provider, so its sign-in can finish.

Stats

What one end counted over one window: the traffic it moved and the round trips it measured. A client reports it to its host every 60 seconds and once more as it leaves.

FieldTypeDescription
windowMsnumberWhat the RttSummary window figures cover; 60000 nominal, shorter for the report leave sends.
trafficarray of TrafficCounterThis client's own counts.
rttRttSummaryRound trip over the window, summarised rather than sample by sample.
pathstringThe selected candidate pair now: "udp" | "tcp" | "turn" | "unknown".
iceRttMsnumberThe engine's figure for the selected pair; 0 if none.
joinToWelcomeMsnumberFirst report only, 0 after: ticket response to Welcome, the one interval the service cannot measure.

StringListPatch

A list you are replacing. Leave the field out to leave the list alone; send this object to replace the list with the values it carries.

FieldTypeDescription
valuesarray of stringThe whole new list. Sent with no values, it clears the list.

TerminationRule

One rule of a session's termination policy: a condition over the facts the service knows about one client, plus how long that condition must hold.

FieldTypeDescription
namestringThis developer's name for the rule, which is what every surface reports the firing under: 1 to 32 characters of [a-z0-9_-], unique in the list.
whenstringThe condition, a CEL expression over the facts the service knows about one client, with no clock in it: the expression grammar is under TerminationRule in the host SDK reference. It is checked when the config is set: at most 512 bytes, 1000 cost units and 16 levels deep, naming declared facts only, and evaluating to bool. An expression outside those bounds is refused validation, with details.field naming the rule and details.reason carrying the checker's own message.
forstringHow long when must hold continuously before the rule fires.

TerminationRuleListPatch

A presence-carrying wrapper for a rule list, the way StringListPatch is for a list of strings: proto3 has no optional on repeated, so a message field stands in.

FieldTypeDescription
valuesarray of TerminationRuleThe whole list, at most 16 rules, in the order a refusal's terminationRules[n] counts them.

Ticket

What one end connects its transport with: where to dial, the credential to dial with, what it may do there, and when it expires.

FieldTypeDescription
sessionIdstringThe session this ticket is for.
participantIdstringThe client this ticket is for, by its id on this wire.
grantsarray of stringWhat this end may do once it is connected. The set is minted into the ticket and cannot be widened by the holder. One of GRANT_SEND_INPUT, GRANT_RECEIVE_INPUT, GRANT_BROADCAST, GRANT_RECEIVE_BROADCAST, GRANT_MANAGE.
protocolnumberThe protocol version the service pinned for this session, which every end of it speaks.
endpointstringWhere the transport connects, the same value GET /v1/whoami answers with. Hand it to the SDK as it is: the transport settles the scheme and the path it needs.
tokenstringA signed token for the media engine, handed to the SDK unchanged.
expiresAtstring (64-bit integer)Unix ms; 60 s after issue.
regionstringThe region the transport is dialled into.
jtistringSingle-use id; consumed by the service on issue and reconciled on join.
openerbooleanTrue for the host.
apiIdstringThe sender identity the service itself signs with in this region, a_<region>: the only sender that is not another client and is still trusted.
kindstringWhich engine endpoint and token are for, fixed when the session opened: custom, the only engine since 0.3. Hand it to the SDK with the rest of the ticket: the SDK picks how to connect from it, and your code never needs to read it.

TokenConfirmation

The client's key thumbprint, bound into a join token for authenticated end-to-end key binding.

FieldTypeDescription
jktstringThe SHA-256 JWK thumbprint of the client's key.

TokenLimits

The limits a mint binds into a token.

FieldTypeDescription
concurrentSessionsnumberDefaults to 1 when the mint omits it.
clientMinutesPerDaynumberHow many client minutes a day this token's subject may spend.
concurrentClientsnumberPer subject; a join token's default is 1.
sessionOpensPerMinutenumberPer subject; default 3.
joinsPerMinutenumberPer subject; default 6.

TokenSummary

One row of GET /v1/tokens.

FieldTypeDescription
tokenIdstringThe token's id, which is what DELETE /v1/tokens/{tokenId} names.
subjectstringWho the token was minted for: your own id for a person or a device.
keyIdstringThe minting key.
permissionsarray of stringWhat the token may do, as a narrowing of the key that minted it.
mintedAtstring (64-bit integer)When the token was minted, Unix ms.
expiresAtstring (64-bit integer)The token's own expiry, before the project's current ceiling.
sessionIdstringA join token's binding, or empty.
grantsCeilingarray of stringA join token's grants ceiling, or empty for none. One of GRANT_SEND_INPUT, GRANT_RECEIVE_INPUT, GRANT_BROADCAST, GRANT_RECEIVE_BROADCAST, GRANT_MANAGE.

TrafficCounter

Traffic one endpoint counted for one client, one topic, one direction and one transport path.

FieldTypeDescription
topicstring"ctl" | "in" | "out".
directionstring"to_host" | "to_client", named from the client's side.
pathstring"udp" | "tcp" | "turn" | "unknown" while these were counted on the engine, and "direct" for the direct link.
messagesCountstring (64-bit integer)How many messages were counted.
bytesstring (64-bit integer)How many bytes were counted.
droppedCountstring (64-bit integer)SDK-side drops.
transportstringThe transport this row's traffic rode, by name: "server", "peer" or a declared transport's name; empty reads as "server".

TrafficTotals

A count of messages and the bytes they carried.

FieldTypeDescription
messagesstring (64-bit integer)How many messages.
bytesstring (64-bit integer)How many bytes, as the host reported them.

TransportTraffic

One transport's host-reported traffic on a session, summed over topics.

FieldTypeDescription
namestringThe transport's name: server, peer, or the name the host declared, such as bluetooth.
inTrafficTotalsWhat went to the host on this transport.
outTrafficTotalsWhat went to clients on this transport.

UsageRow

One bucket of usage: when it starts, and what each meter counted in it.

FieldTypeDescription
bucketStartstring (64-bit integer)Start of the UTC day or month.
metersobject, string keys to numberMeter name -> value, in the metering vocabulary.

Next steps