REST API
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 wire | The word this site uses |
|---|---|
participants, participantId, the participant-state values they carry, the participant_minutes meter, ParticipantView, ParticipantStats, ParticipantMeters | client |
seat, seatClass, seatDeadlineMs, the seat_spent error code | a client's place |
the "admin" value of a key's type, the admin_key audit actor, the /admin-keys routes | organization 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 for | What it is |
|---|---|
| publishable key | Starts 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 key | Starts 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. |
| token | Starts 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 key | Starts bsk_ak_. The management credential: projects, keys, limits, settings and the audit log, across an organization. |
| host token | Starts 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 token | Starts bsk_rt_. Held by a client so it can return to the session it already joined. |
| console sign-in | Starts bsk_cs_. What the console holds after you sign in. It opens no session, and an integration never holds one. |
| manage | Not 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 credential | 12 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.
| Header | What it carries |
|---|---|
Authorization | Bearer <credential>. The one header every credential travels in. |
Accept | application/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-Type | application/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-Project | The 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-Key | Optional, 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-SDK | Optional. 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.
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.
| Field | What it carries |
|---|---|
code | The one field to branch on. Every value it takes is in the table at the end of this page. |
message | A sentence for a person. Never parse it. |
retryable | Whether sending this same request again can work. |
retryAfterMs | How long to wait first. The Retry-After header carries the same wait in seconds. |
requestId | The handle support traces a refusal by. Also the X-Bitskiff-Request-Id response header. |
closeCode | Present only on a refusal that ended a session or a client: the code it was closed with. A read carries none, whatever it refuses. |
details | What 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 key | Type | What it carries |
|---|---|---|
field | string | The 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. |
option | string | The 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. |
header | string | The request header at fault, in its canonical spelling: Content-Type, Accept, Idempotency-Key, X-Bitskiff-Project, X-Bitskiff-Key. |
path | string | <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. |
named | string | What an out-of-reach read asked for, on the one not_found step 4. |
sessionId | string | The session id the request named, as it was sent, on session_not_found. |
reason | string | Which rule refused, from the vocabulary in the table below. |
class | string | The 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. |
credential | string | The prefix of the credential that was presented (bsk_pk_), never the credential itself. |
permission | string | The permission the route asked for and the credential does not hold, on a missing_permission refusal. |
object | string | The 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. | |
keyId | string | The key a refusal is about, on the own_line and not_descendant refusals. |
role | string | The organization role a key may not hand out, on a role_above_key refusal. |
policy | string | The organization policy that refused the act, on a policy refusal: orgKeyMinting or deviceSignIn. |
expiredAt | unixMs | When the organization's plan expired, Unix milliseconds, on plan_expired to the developer; a page never receives it. |
supported | string | The protocol versions this service does speak, on protocol_unsupported. |
ceiling | integer | A 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.reason | On | What it says |
|---|---|---|
absent | invalid_key, origin_not_allowed | Nothing 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. |
malformed | invalid_key | A credential was presented and carries none of the prefixes. |
unknown | invalid_key, invalid_token, handoff_invalid | Well-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. |
revoked | invalid_key, invalid_token | This deployment issued it and it has been revoked; mint a fresh one. |
spent | handoff_invalid | The handoff ticket was already redeemed once, or a final refusal naming it spent it; the app shows one line and its scanner. |
superseded | handoff_invalid | The page minted a later handoff ticket for the same place, and one is pending at a time; the later ticket is the live one. |
cancelled | handoff_invalid | The page cancelled the handoff before this redeem (DELETE .../clients/{clientId}/handoff); the place stays the page's. |
lapsed | handoff_invalid | On 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. |
expired | invalid_key, invalid_token, handoff_invalid | Past its expiresAt; mint a fresh one. |
mismatch | origin_not_allowed | The 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_project | wrong_client | The 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_host | wrong_client | The 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_class | forbidden | The credential is well-formed and its class may not call this route; details.class names the classes that may. |
missing_permission | forbidden | The class admits the route and the effective permission list does not carry the word; details.permission names it. |
own_line | forbidden | A key acted on itself or an ancestor, which is what stops a leaked key rotating the key its owner would stop it with. |
not_descendant | forbidden | A 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. |
policy | forbidden | An 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_archived | forbidden | The 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_closed | forbidden | The project was archived more than 30 days ago, so it can no longer be unarchived; details.object names it. |
role_above_key | forbidden | An organization key invited a role above its own, or changed or removed a member who holds one; details.role names it. |
own_role | forbidden | A console user tried to change their own organization role; another admin or owner changes it. |
other_app | wrong_client | On 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_attached | forbidden | On 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_session | forbidden | On 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_required | forbidden | The 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_session | forbidden | A join token bound to one session was presented at a join into another session of its project, whatever the require_token row says. |
grant_expired | invalid_code | The join grant presented on POST /v1/join is past its joinGrantTtl or was never minted; re-entry is another scan. |
grant_used | invalid_code | The 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_mismatch | invalid_code | The 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_open | invalid_code | A 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_transferable | forbidden | The invitation was sent to another address. |
device_code_denied | forbidden | The person on the approval page pressed Cancel, so this device code will never sign anybody in; a terminal stops polling at once. |
email_unverified | forbidden | On 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_closed | forbidden | On 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_required | forbidden | On 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_use | forbidden | On 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_required | forbidden | The 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_up | forbidden | The console user has a second factor, but did not pass it within identity.stepUpWindowS; pass it again and retry once. |
staff_access_refused | forbidden | A 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_public | forbidden | Which rule a webhook URL broke: the six are the whole of the policy. |
duration | validation | A 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_api | unavailable | Raised 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_owner | validation | Removing or demoting this member would leave the organization with no owner; make another member an owner first. |
last_default | validation | The billing account is the organization's default and an organization keeps exactly one; make another billing account the default instead. |
invoice_status | validation | The 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_ended | validation | An invoice is issued for a calendar month (UTC) that has ended; name an earlier month. |
max_members_per_org | validation | The organization is at its member ceiling; details.ceiling carries the number. |
| an end reason | session_ended, seat_spent | Why 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:
{
"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:
{
"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:
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
config | SessionConfig | no | Partial; 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. |
label | string | no | Your 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. |
metadata | object, string keys to string | no | Your 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>. |
protocol | ProtocolRange | no | The 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. |
sdk | string | no | Same value as X-Bitskiff-SDK. |
channels | array of ChannelRow | no | The host's presented channel table: every row the host declared before open, ids assigned. |
fixedCodeId | string | no | The id of the fixed code this session claims, the host SDK's fixedCode open option; empty claims nothing. |
origin | string | no | Where a native build says it is: the custom scheme its host key lists, com.studio.racer://host. |
Response body
| Field | Type | Description |
|---|---|---|
session | SessionView | The session that was opened: its config as it was clamped, the ceilings it was clamped against, and its state. |
hostToken | string | The 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. |
hostTicket | Ticket | The host's own ticket, carrying the GRANT_RECEIVE_INPUT, GRANT_BROADCAST and GRANT_MANAGE grants. |
joinCode | JoinCodeMaterial | The 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. |
serverTimeMs | string (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. |
limits | SessionLimits | The caps this session's clients are held to. |
capacity | CapacityNotice | The 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. |
channels | array of ChannelRow | The effective channel table: the presented rows, defaults filled and clamped. |
stunUrls | array of string | Bitskiff's STUN URLs: the relay's stun: and stuns: entries and no credential, the servers a peer offer carries. |
Example request
{
"config": {
"maxClients": 2
},
"label": "lobby 14"
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
protocol | ProtocolRange | no | The 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. |
sdk | string | no | Which SDK is calling and its version, such as ts-host/0.1.0. It is carried on the session's own rows. |
channels | array of ChannelRow | no | A 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. |
evictedEpoch | number | no | Set 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. |
lostNode | LostNode | no | Set only by a host whose link the custom engine ended with bye node_lost or server_shutdown: the process that bye named. |
Response body
| Field | Type | Description |
|---|---|---|
session | SessionView | The session this ticket is for: its config as it was clamped, the ceilings it was clamped against, and its state. |
hostTicket | Ticket | What the host connects its transport with. |
hostToken | string | Rotated; the predecessor stays good until the successor is first presented, and no clock ends it before that. |
joinCode | JoinCodeMaterial | The 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. |
serverTimeMs | string (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. |
limits | SessionLimits | Re-sent on every host ticket, so a host that resumes re-reads it. |
channels | array of ChannelRow | The session's effective channel table, answered when the presented table, clamped under the session's key, matched it field by field. |
replay | boolean | True when the request named an evicted epoch and it had not moved: the eviction was a replayed ticket, and this is the ordinary resume. |
relocation | Relocation | Present when the request carried lostNode and the session was re-placed, by this request or an earlier one; absent on every other answer. |
stunUrls | array of string | Bitskiff's STUN URLs, as OpenSessionResponse.stunUrls`: re-sent on every host ticket, so a host that resumes re-reads them. | |
Example request
{
"protocol": {
"min": 1,
"max": 1
},
"sdk": "ts-host/0.2.0"
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
protocol | ProtocolRange | no | The 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. |
sdk | string | no | Which SDK is calling and its version, such as ts-host/0.1.0. It is carried on the session's own rows. |
channels | array of ChannelRow | no | A 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. |
evictedEpoch | number | no | Set 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. |
lostNode | LostNode | no | Set only by a host whose link the custom engine ended with bye node_lost or server_shutdown: the process that bye named. |
Response body
| Field | Type | Description |
|---|---|---|
session | SessionView | The session this ticket is for: its config as it was clamped, the ceilings it was clamped against, and its state. |
hostTicket | Ticket | What the host connects its transport with. |
hostToken | string | Rotated; the predecessor stays good until the successor is first presented, and no clock ends it before that. |
joinCode | JoinCodeMaterial | The 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. |
serverTimeMs | string (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. |
limits | SessionLimits | Re-sent on every host ticket, so a host that resumes re-reads it. |
channels | array of ChannelRow | The session's effective channel table, answered when the presented table, clamped under the session's key, matched it field by field. |
replay | boolean | True when the request named an evicted epoch and it had not moved: the eviction was a replayed ticket, and this is the ordinary resume. |
relocation | Relocation | Present when the request carried lostNode and the session was re-placed, by this request or an earlier one; absent on every other answer. |
stunUrls | array of string | Bitskiff's STUN URLs, as OpenSessionResponse.stunUrls`: re-sent on every host ticket, so a host that resumes re-reads them. | |
Example request
{
"protocol": {
"min": 1,
"max": 1
},
"sdk": "ts-host/0.2.0"
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
protocol | ProtocolRange | no | The protocol versions the calling SDK can speak. A resume is refused protocol_unsupported when the session's pinned version is outside this range. |
sdk | string | no | Which SDK is calling and its version, such as ts-client/0.1.0. |
evictedJti | string | no | Set 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. |
lostNode | LostNode | no | Set only by a client whose link the custom engine ended with bye node_lost or server_shutdown: the process that bye named. |
environment | ClientEnvironment | no | As JoinRequest.environment. |
metadata | object, string keys to string | no | As 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
| Field | Type | Description |
|---|---|---|
ticket | Ticket | What this client connects its transport with: a fresh one, for the connection it is making now. |
resumeToken | string | Token rotated. |
seatDeadlineMs | string (64-bit integer) | Unix ms on the service's clock past which this resume token is refused and the client's place is gone. |
serverTimeMs | string (64-bit integer) | The service's own Unix ms at the moment it answered. |
limits | SessionLimits | Re-sent on every resume, so a cap change reaches a live client. |
policyDeadlineMs | string (64-bit integer) | As JoinResponse.policyDeadlineMs, read again at the resume. |
replay | boolean | True when the resume named an evicted ticket and nothing was minted since: the eviction was a replayed ticket, and this is the ordinary resume. |
handoffFailed | string | On 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. |
registeredApps | array of RegisteredApp | The 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
{
"protocol": {
"min": 1,
"max": 1
},
"sdk": "ts-client/0.2.0"
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
hint | HandoffHint | no | The page's transport as it stands at the mint; absent when it states none. |
dataText | string | no | The handoff data as a string. |
dataBytes | string (base64) | no | The handoff data as bytes. |
hostIdentityPublic | string (base64) | no | In 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
| Field | Type | Description |
|---|---|---|
ticket | string | The handoff ticket, bsk_hf_ and 43 characters: carried in the handoff link's fragment only. |
expiresAtMs | string (64-bit integer) | Unix ms on the service's clock past which the ticket is refused handoff_invalid, expired. |
serverTimeMs | string (64-bit integer) | The service's own Unix ms at the moment it answered; the client reads expiresAtMs against it. |
Example request
{
"hint": {
"transport": "peer",
"grade": "good",
"rttMs": 12
},
"dataText": "player-7"
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
presence | string | yes | What 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. |
jti | string | no | The 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
| Field | Type | Description |
|---|---|---|
presence | string | The presence the service recorded. One of PRESENCE_PRESENT, PRESENCE_AWAY. |
backgroundMsLeft | number | How much of the background budget is left, in milliseconds. Zero once it is spent. |
hostDeadlineMs | string (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
{
"presence": "PRESENCE_AWAY"
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | no | Why 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
| Field | Type | Description |
|---|---|---|
presence | string | The presence the service recorded. One of PRESENCE_PRESENT, PRESENCE_AWAY. |
backgroundMsLeft | number | How much of the background budget is left, in milliseconds. Zero once it is spent. |
hostDeadlineMs | string (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
{
"reason": "host_unverified"
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
engineSid | string | yes | The engine sid the host last heard this client on. |
lastHeardMs | string (64-bit integer) | no | Unix ms on the host's own clock; the service logs it and decides nothing on it. |
query | boolean | no | A question, not a report: the service commits nothing and answers where the record stands and the budget it is running. |
Response body
| Field | Type | Description |
|---|---|---|
detached | boolean | This report committed the detach. |
connection | string | The record's connection after the report: "attached", "detached", "reserved" or "gone"; empty when there is no record. |
budgetDeadlineMs | string (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
{
"engineSid": "PA_7c2f1e9d",
"lastHeardMs": "1774000003250"
}
Example response
{}
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
| Field | Type | Description |
|---|---|---|
connection | string | The record's connection: "attached", "detached", "reserved" or "gone"; empty when there is no record. |
budgetDeadlineMs | string (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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
transport | string | yes | The name of the transport that carries the client for the span. |
ifLost | string | no | The client's choice for a lost link, from its Release. One of RELEASE_IF_LOST_REJOIN, RELEASE_IF_LOST_END. |
Response body
| Field | Type | Description |
|---|---|---|
connection | string | The record's connection after the call: "released" when the mark committed or was already there, "gone" for a place that is over. |
releasedAtMs | string (64-bit integer) | Absolute Unix ms on the service's clock the place was marked released. |
Example request
{
"transport": "wifi_direct",
"ifLost": "RELEASE_IF_LOST_REJOIN"
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
outcome | string | yes | "lost": the link died and the client rejoins, so the service detaches the place and arms its reconnect budget. |
reason | string | no | For "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). |
releasedAtMs | string (64-bit integer) | no | The release this report is about, as ReleaseResponse answered it. |
Response body
| Field | Type | Description |
|---|---|---|
applied | boolean | This report moved the place. |
connection | string | The record's connection after the report: "released", "detached", "attached" or "gone". |
budgetDeadlineMs | string (64-bit integer) | The reconnect budget a "lost" report armed: absolute Unix ms on the service's clock, 0 when none. |
Example request
{
"outcome": "lost",
"releasedAtMs": "1774000003250"
}
Example response
{
"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
| Field | Type | Description |
|---|---|---|
released | boolean | The session is host_released after this call. |
releasedAtMs | string (64-bit integer) | Absolute Unix ms on the service's clock the session went host_released. |
deadlines | SessionDeadlines | The deadlines a released host holds on its own clock: max_time and the released ceiling. |
blockingClients | array of string | When not released: the held clients that are not. |
Example response
{
"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
| Field | Type | Description |
|---|---|---|
connection | string | The record's connection: "reserved", "attached", "detached", "released" or "gone". |
releasedAtMs | string (64-bit integer) | When the place was marked released: absolute Unix ms on the service's clock; 0 unless released. |
releasedTransport | string | The transport the host named at the release; empty unless released. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
clientId | string | no | The client the offer is for; empty is the host's own side. |
Response body
| Field | Type | Description |
|---|---|---|
urls | array of string | The relay's turn: URLs, udp and tcp. |
username | string | {expiry_unix}:{participantId}, naming no project and no key. |
credential | string | Base64 HMAC-SHA1 of username under the relay's shared secret. |
ttlSeconds | number | The session's idle limit or 12 hours, whichever is shorter. |
Example request
{
"clientId": "p_MWnCKAv7YT4UCIX8ncPLUA"
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
closingSid | string | yes | The engine sid of the connection the client is closing. |
Response body
None. The service answers 204.
Example request
{
"closingSid": "PA_7c2f1e9d"
}
Example response
{}
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
| Field | Type | Required | Description |
|---|---|---|---|
windowStart | string (64-bit integer) | yes | Unix ms; start of the interval this report covers. |
windowEnd | string (64-bit integer) | yes | Unix ms. |
seq | number | yes | Per 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. |
host | Stats | no | The host's own counters. |
participants | array of ParticipantStats | no | One entry per client the host counted traffic for over this window. |
control | array of ControlCount | no | The 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
| Field | Type | Description |
|---|---|---|
capacity | CapacityNotice | The 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
{
"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
{}
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
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | no | Max 128 chars; becomes ended.reason detail. |
Response body
The SessionView object, written out under Objects.
Example request
{
"reason": "the shift ended"
}
Example response
{
"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.
| Field | Type | Required | Description |
|---|---|---|---|
subject | string | no | Keep only the sessions this subject opened. Omit it for every subject. A token reads its own subject and nothing else, whatever it sends here. |
label | string | no | Keep 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. |
cursor | string | no | Where 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. |
limit | number | no | How 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. |
hostState | string | no | Keep 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. |
clientSubject | string | no | Keep 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
| Field | Type | Description |
|---|---|---|
sessions | array of SessionView | One session per row, as far as this page goes. |
nextCursor | string | Where 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
GET /v1/sessions?label=lobby%2014&limit=20
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
subject | string | yes | The 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. |
reason | string | no | Why 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
| Field | Type | Description |
|---|---|---|
booted | number | How many places this call freed. 0 when the subject held none, which is what a repeat answers. |
Example request
{
"subject": "user_8123",
"reason": "BOOT_REASON_POLICY"
}
Example response
{
"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
{
"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
| Field | Type | Description |
|---|---|---|
sessionId | string | The session these meters were read from. |
readAtMs | string (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. |
session | object, string keys to string | The 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. |
participants | array of ParticipantMeters | One 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. |
transports | array of TransportTraffic | The 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. |
control | array of ControlCount | The 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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | no | Why 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
{
"reason": "BOOT_REASON_BOOTED"
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
grants | array of string | yes | Replaces 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
{
"grants": [
"GRANT_SEND_INPUT",
"GRANT_RECEIVE_BROADCAST",
"GRANT_MANAGE"
]
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
subject | string | yes | The developer's own user or device id, 1 to 128 characters. |
limits | TokenLimits | no | Optional; the limits this token's presentations are held to. |
ttl | string | no | How 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. |
sdk | string | no | Same value as X-Bitskiff-SDK. |
permissions | array of string | no | What 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. |
sessionId | string | no | Optional, sessions:join only: this session and no other. |
cnf | TokenConfirmation | no | Optional, sessions:join only. |
grantsCeiling | array of string | no | Optional, 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
| Field | Type | Description |
|---|---|---|
tokenId | string | tok_..., the id GET and DELETE address it by. |
token | string | bsk_at_..., returned once and stored only as its SHA-256. |
expiresAt | string (64-bit integer) | Unix ms. |
Example request
{
"subject": "user_8123",
"permissions": [
"sessions:open"
],
"ttl": "1h"
}
Example response
{
"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.
| Parameter | Type | Required | Description |
|---|---|---|---|
subject | string | no | Keep only the tokens minted for this subject. Omit it for every subject. |
keyId | string | no | Keep only the tokens minted by this key. Omit it for every key. |
cursor | string | no | Where 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. |
limit | number | no | How 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
| Field | Type | Description |
|---|---|---|
tokens | array of TokenSummary | One live token per row, and never a token string. |
nextCursor | string | Where 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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
code | string | yes | The join code, as the client read it out of its link. |
sdk | string | no | Which SDK is calling and its version, such as ts-client/0.1.0. |
origin | string | no | Where this client says it is, declared once on the BitskiffClient: a URL or a custom scheme. |
reserve | JoinReserve | no | Gated 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
| Field | Type | Description |
|---|---|---|
sessionId | string | The session the code belongs to. |
state | string | A joinable session state: "open" | "host_attached" | "host_away". |
clientsFree | number | How 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. |
region | string | The region the session runs in. |
protocol | number | The session's pinned version. |
maxClients | number | The total clientsFree is counted against: SessionConfig.maxClients as the session answered it. |
e2ee | boolean | End-to-end encryption: the session's own mode, fixed at open. |
requireToken | string | Whether 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. |
grant | JoinGrant | The join grant a request carrying reserve asked for; absent when none was asked for, and when the reservation was refused. |
reservationRefused | ReservationRefused | Why a reservation was refused, when it was. |
Example request
{
"code": "1CAbCdEfG1HPWtg",
"sdk": "ts-client/0.2.0"
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
code | string | no | The join code, as the client read it out of its link. |
grant | JoinGrantEntry | no | The join grant POST /v1/join/inspect reserved from a fresh code, and its holder secret. |
protocol | ProtocolRange | no | The protocol versions the calling SDK can speak. A join is refused protocol_unsupported when the session's pinned version is outside this range. |
sdk | string | no | Which SDK is calling and its version, such as ts-client/0.1.0. |
priorClientId | string | no | Best-effort continuity hint; not an identity claim. |
e2ee | boolean | no | The 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. |
origin | string | no | Where 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. |
environment | ClientEnvironment | no | The program this client runs in, stated by its SDK (ClientEnvironment). |
metadata | object, string keys to string | no | The 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
| Field | Type | Description |
|---|---|---|
ticket | Ticket | What this client connects its transport with. |
resumeToken | string | What this client presents to take its place back after a drop. Every resume rotates it. |
reconnectBudgetMs | number | How long this client may spend reconnecting before its place is given up. |
backgroundBudgetMs | number | How long this client may spend backgrounded before its place is given up. |
seatDeadlineMs | string (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. |
serverTimeMs | string (64-bit integer) | The service's own Unix ms at the moment it answered. |
limits | SessionLimits | The caps this client is held to; a client holds no project key and so never calls the limits route. |
policyDeadlineMs | string (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. | |
registeredApps | array of RegisteredApp | The 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. |
handoffHint | HandoffHint | On the answer to POST /v1/handoff only: the page's transport as it stated it on the mint, absent when it stated none. |
handoffDataText | string | The handoff data, passed as a string. |
handoffDataBytes | string (base64) | The handoff data, passed as bytes. |
e2ee | boolean | On the answer to POST /v1/handoff only: whether the place runs end-to-end encryption, the session's own fixed value (SessionView.e2ee). |
hostIdentityPublic | string (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
{
"code": "1CAbCdEfG1HPWtg",
"protocol": {
"min": 1,
"max": 1
},
"sdk": "ts-client/0.2.0"
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
ticket | string | yes | The handoff ticket the page put in the launch link's fragment, read by the app and sent once. |
protocol | ProtocolRange | no | As JoinRequest.protocol: the protocol range this SDK speaks. |
sdk | string | no | As JoinRequest.sdk: the SDK's name and version, for the inventory. |
origin | string | no | As JoinRequest.origin: the app's declared origin, read first, the Origin header when empty. |
e2ee | boolean | no | As JoinRequest.e2ee: the app's capability, never the fingerprint. |
keyThumbprint | string | no | The RFC 7638 SHA-256 thumbprint of the X25519 key the app's Hello will present, the value client.keyThumbprint() gives. |
environment | ClientEnvironment | no | As JoinRequest.environment: the app's own. |
metadata | object, string keys to string | no | As 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
| Field | Type | Description |
|---|---|---|
ticket | Ticket | What this client connects its transport with. |
resumeToken | string | What this client presents to take its place back after a drop. Every resume rotates it. |
reconnectBudgetMs | number | How long this client may spend reconnecting before its place is given up. |
backgroundBudgetMs | number | How long this client may spend backgrounded before its place is given up. |
seatDeadlineMs | string (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. |
serverTimeMs | string (64-bit integer) | The service's own Unix ms at the moment it answered. |
limits | SessionLimits | The caps this client is held to; a client holds no project key and so never calls the limits route. |
policyDeadlineMs | string (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. | |
registeredApps | array of RegisteredApp | The 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. |
handoffHint | HandoffHint | On the answer to POST /v1/handoff only: the page's transport as it stated it on the mint, absent when it stated none. |
handoffDataText | string | The handoff data, passed as a string. |
handoffDataBytes | string (base64) | The handoff data, passed as bytes. |
e2ee | boolean | On the answer to POST /v1/handoff only: whether the place runs end-to-end encryption, the session's own fixed value (SessionView.e2ee). |
hostIdentityPublic | string (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
{
"ticket": "bsk_hf_...",
"protocol": {
"min": 1,
"max": 1
},
"sdk": "android-client/0.2.0"
}
Example response
{
"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
| Field | Type | Description |
|---|---|---|
registeredApps | array of RegisteredApp | The project's registered apps, one per platform, with clientKeyId and fingerprints empty. |
Example response
{
"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
| Field | Type | Description |
|---|---|---|
keys | array of KeyView | One 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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
type | string | yes | "publishable" | "secret". |
label | string | no | Your own name for the key. It is what the console and the audit rows show. |
origins | array of string | no | The origins a publishable key may be used from. A page on any other origin is refused origin_not_allowed. |
limits | KeyLimits | no | Caps this key is held to, each at or below the project's own. |
expiresAt | string (64-bit integer) | no | When the key expires, Unix ms; 0 takes the default. |
permissions | array of string | no | What 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. |
preset | string | no | One of the console's ready-made permission lists instead of your own: "full-access" | "host-only" | "monitor". Name this or permissions, never both. |
hostKeys | array of string | no | A client key only: the ids of the host keys of the same project whose sessions it may join. |
subjects | array of string | no | A 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
{
"type": "secret",
"label": "backend",
"permissions": [
"sessions:open",
"tokens:mint"
]
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
retireAt | string (64-bit integer) | no | Move when a rotating key stops being accepted, Unix ms. Omit it to leave it alone. |
label | string | no | A new name for the key. Omit it to leave the name alone. |
origins | StringListPatch | no | Replace the origin list. Omit it to leave the list alone; send it with no values to clear it. |
limits | KeyLimits | no | New caps for the key. Each field applies on its own, and one left out is left alone. |
expiresAt | string (64-bit integer) | no | Bring 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. |
permissions | StringListPatch | no | Secret keys only; validates exactly as POST /project/keys does. |
hostKeys | StringListPatch | no | A client key only, validated as the create does. |
preset | string | no | A secret key's preset instead of permissions ("full-access", "host-only", "monitor"), expanded before the write; naming both is a validation refusal. |
subjects | StringListPatch | no | A secret key holding tokens:mint only, validated as the create does. |
Response body
The KeyView object, written out under Objects.
Example request
{
"label": "backend (eu)",
"origins": {
"values": [
"https://example.com"
]
}
}
Example response
{
"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.
| Parameter | Type | Required | Description |
|---|---|---|---|
reason | string | yes | "manual" | "compromise". There is no default: leaving it out, or sending anything else, is refused validation with details.field reason. |
Response body
| Field | Type | Description |
|---|---|---|
key | KeyView | The key as it now stands, revoked. |
sessionsEndedCount | string (64-bit integer) | How many sessions this key had open were ended with it. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
overlapS | number | no | How 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
{
"overlapS": 86400
}
Example response
{
"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
| Field | Type | Description |
|---|---|---|
fixedCodes | array of FixedCodeView | This page's fixed codes, oldest first. |
nextCursor | string | Empty when this page is the last. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
label | string | no | At most 64 UTF-8 bytes; nothing resolves by it. |
Response body
The FixedCodeView object, written out under Objects.
Example request
{
"label": "front door"
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
label | string | no | The new label, at most 64 UTF-8 bytes. |
Response body
The FixedCodeView object, written out under Objects.
Example request
{
"label": "bar, left of the door"
}
Example response
{
"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
{
"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
| Field | Type | Description |
|---|---|---|
ceilings | SessionConfig | The 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. |
maxConcurrentSessions | number | The most sessions this project may hold open at once. |
maxConcurrentClients | number | The most clients this project may hold at once. It is the number a join meets before a session's own maxClients. |
concurrentSessions | number | How many sessions this project has open right now, counted against maxConcurrentSessions. |
concurrentClients | number | How many clients this project holds right now, counted against maxConcurrentClients. A host does not count. |
sessionOpensPerMinute | number | How many sessions this project may open in a minute. |
joinsPerMinuteSession | number | The joinsPerMinuteSession row, spelled here the way details.limit, LimitsPatch and bitskiff limits set spell it. |
bytesPerSecond | number | The one per-client byte cap: the bytesPerSecond row, per client per direction. |
clientMinutesPerDay | LimitQuota | Day counter, resets 00:00 UTC. |
monthlySpendCap | LimitQuota | The 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. |
turnBytesPerMonth | LimitQuota | Bytes, month rollover. |
ticketMintsPerMinute | LimitQuota | Key-scoped rate limit; used and periodEnd are 0. |
clientResumesPerMinute | number | How many times one client may resume in a minute. |
statsReportsPerMinuteSession | number | How many stats reports one session may post in a minute. |
tokensMaxTtl | string | The 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. |
joinsPerMinuteIp | number | How many joins this project admits from one address in a minute—20 unless the project has raised it, up to 200. |
joinsPerMinuteKey | number | The joinsPerMinuteKey row: joins one client key admits in a minute, across every session and address it joins from. |
messagesPerSecond | number | The messagesPerSecond row's bitskiff.in rate: the project's own value, 120 unless the project has lowered it. |
linkMessagesPerSecond | number | The 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. |
bootCooldown | string | bootCooldown: 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). |
requireToken | string | Whether 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. |
samplePageSessions | SamplePageSessions | How 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
{
"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
{
"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
| Field | Type | Description |
|---|---|---|
joinUrl | string | The page a join code points a client at. |
e2ee | string | "off" | "required". |
warnings | array of string | Non-fatal notes, such as a joinUrl no key's allowlist covers. |
restrictedMode | boolean | Publishable keys cannot open new sessions while this is on. |
terminationRules | array of TerminationRule | The project's default termination policy: the rule list a session takes when its open names none. |
sessionDefaults | SessionDefaults | The project's session defaults: the value each session-config field takes when an open leaves it out. |
registeredApps | array of RegisteredApp | The native apps the project registered, one per platform at most. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
joinUrl | string | no | A new join URL. Omit it to leave the setting alone. |
e2ee | string | no | "off" | "required". |
restrictedMode | boolean | no | Reversible, and nothing live ends. |
terminationRules | TerminationRuleListPatch | no | The project's default termination rule list, replaced whole. |
sessionDefaults | SessionDefaults | no | The 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. |
registeredApps | RegisteredAppListPatch | no | The registered apps, replaced whole. |
Response body
| Field | Type | Description |
|---|---|---|
joinUrl | string | The page a join code points a client at. |
e2ee | string | "off" | "required". |
warnings | array of string | Non-fatal notes, such as a joinUrl no key's allowlist covers. |
restrictedMode | boolean | Publishable keys cannot open new sessions while this is on. |
terminationRules | array of TerminationRule | The project's default termination policy: the rule list a session takes when its open names none. |
sessionDefaults | SessionDefaults | The project's session defaults: the value each session-config field takes when an open leaves it out. |
registeredApps | array of RegisteredApp | The native apps the project registered, one per platform at most. |
Example request
{
"joinUrl": "https://example.com/join"
}
Example response
{
"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.
| Field | Type | Required | Description |
|---|---|---|---|
maxClients | number | no | The clients each session holds; 0 reads the project's maxClients ceiling. |
lossyHz | number | no | The summed maxHz of the lossy rows each session declares; 0 is none. |
reliableHz | number | no | The summed maxHz of the reliable rows each session declares; 0 is none. |
maxBytes | number | no | A message's ceiling on those rows; 0 is the wire ceiling, as an open row typing none. |
Response body
| Field | Type | Description |
|---|---|---|
standing | CapacityNotice | Always present; OK over an empty list when nothing stands. |
sessions | number | How 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. |
clients | number | The clients those sessions hold, held to what concurrentClients leaves. |
maxClients | number | The maxClients the reading priced each session at. |
Example query
GET /v1/project/capacity?maxClients=4&lossyHz=30
Example response
{
"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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | The 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
{
"name": "Acme lobby"
}
Example response
{
"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
{
"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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
concurrentSessions | number | no | A 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. |
concurrentClients | number | no | A 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. |
sessionOpensPerMinute | number | no | A new ceiling on session opens per minute. Omit it to leave the current one. |
joinsPerMinuteSession | number | no | A 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. |
clientResumesPerMinute | number | no | A new ceiling on resumes per minute for one client. Omit it to leave the current one. |
statsReportsPerMinuteSession | number | no | A new ceiling on stats reports per minute for one session. Omit it to leave the current one. |
clientMinutesPerDay | string (64-bit integer) | no | A new cap on client minutes a day. Omit it to leave the current one. |
monthlySpendCap | string (64-bit integer) | no | A new monthly spend cap, in rated units. Omit it to leave the current one. |
turnBytesPerMonth | string (64-bit integer) | no | A new cap on relayed bytes a month. Omit it to leave the current one. |
ticketMintsPerMinute | number | no | A new ceiling on tickets minted per minute. Omit it to leave the current one. |
joinsPerMinuteKey | number | no | A client key's join window, project-wide. |
messagesPerSecond | number | no | The bitskiff.in rate, lowered only (at most 120). |
linkMessagesPerSecond | number | no | The link ceiling, raised per plan. |
bootCooldown | string | no | The 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. |
bytesPerSecond | number | no | The byte row per client per direction, raised up to the plan's ceiling; a client key's own value sits under it. |
Response body
| Field | Type | Description |
|---|---|---|
ceilings | SessionConfig | The 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. |
maxConcurrentSessions | number | The most sessions this project may hold open at once. |
maxConcurrentClients | number | The most clients this project may hold at once. It is the number a join meets before a session's own maxClients. |
concurrentSessions | number | How many sessions this project has open right now, counted against maxConcurrentSessions. |
concurrentClients | number | How many clients this project holds right now, counted against maxConcurrentClients. A host does not count. |
sessionOpensPerMinute | number | How many sessions this project may open in a minute. |
joinsPerMinuteSession | number | The joinsPerMinuteSession row, spelled here the way details.limit, LimitsPatch and bitskiff limits set spell it. |
bytesPerSecond | number | The one per-client byte cap: the bytesPerSecond row, per client per direction. |
clientMinutesPerDay | LimitQuota | Day counter, resets 00:00 UTC. |
monthlySpendCap | LimitQuota | The 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. |
turnBytesPerMonth | LimitQuota | Bytes, month rollover. |
ticketMintsPerMinute | LimitQuota | Key-scoped rate limit; used and periodEnd are 0. |
clientResumesPerMinute | number | How many times one client may resume in a minute. |
statsReportsPerMinuteSession | number | How many stats reports one session may post in a minute. |
tokensMaxTtl | string | The 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. |
joinsPerMinuteIp | number | How many joins this project admits from one address in a minute—20 unless the project has raised it, up to 200. |
joinsPerMinuteKey | number | The joinsPerMinuteKey row: joins one client key admits in a minute, across every session and address it joins from. |
messagesPerSecond | number | The messagesPerSecond row's bitskiff.in rate: the project's own value, 120 unless the project has lowered it. |
linkMessagesPerSecond | number | The 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. |
bootCooldown | string | bootCooldown: 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). |
requireToken | string | Whether 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. |
samplePageSessions | SamplePageSessions | How 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
{
"concurrentSessions": 25,
"sessionOpensPerMinute": 60
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
limit | string | yes | Which limit to raise, by the name a refusal carries in details.limit, such as concurrentSessions. |
value | string (64-bit integer) | yes | What to raise it to. |
keyId | string | no | Raise the limit on one key rather than on the project. Omit it for the project. |
word | string | no | The 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
| Field | Type | Description |
|---|---|---|
limit | string | The limit this answer is about. |
value | string (64-bit integer) | The value that was asked for. |
applied | boolean | True when that value is in force now. It is true on every raise: no plan carries a ceiling, so nothing is held back. |
ceiling | string (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. |
pending | boolean | True 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. |
word | string | The word now in force, when the row takes a word. value is then 0, and the raise is never pending. |
Example request
{
"limit": "concurrentSessions",
"value": "50"
}
Example response
{
"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
| Field | Type | Description |
|---|---|---|
raises | array of LimitRaiseView | The pending raises, by project and then by limit. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
projectId | string | yes | The project whose raise this resolves, as proj_.... |
limit | string | yes | The limit in its parameter spelling (concurrentSessions), as the list names it. |
reason | string | no | Why, 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
| Field | Type | Description |
|---|---|---|
projectId | string | The project whose raise was resolved. |
limit | string | The limit in its parameter spelling. |
value | string (64-bit integer) | The value the raise asked for, which an approve applied. |
approved | boolean | true for an approve, which applied the value; false for a deny, which left the current value in place. |
reason | string | The reason the owner gave, empty when none was. |
Example request
{
"projectId": "proj_g1qr384an9asb4kdp10e",
"limit": "concurrentSessions",
"reason": "the plan now covers it"
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
projectId | string | yes | The project whose raise this resolves, as proj_.... |
limit | string | yes | The limit in its parameter spelling (concurrentSessions), as the list names it. |
reason | string | yes | Why, 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
| Field | Type | Description |
|---|---|---|
projectId | string | The project whose raise was resolved. |
limit | string | The limit in its parameter spelling. |
value | string (64-bit integer) | The value the raise asked for, which an approve applied. |
approved | boolean | true for an approve, which applied the value; false for a deny, which left the current value in place. |
reason | string | The reason the owner gave, empty when none was. |
Example request
{
"projectId": "proj_g1qr384an9asb4kdp10e",
"limit": "concurrentSessions",
"reason": "not this month"
}
Example response
{
"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.
| Field | Type | Required | Description |
|---|---|---|---|
from | string (64-bit integer) | yes | Ms since epoch, inclusive. |
to | string (64-bit integer) | yes | Ms since epoch, exclusive. |
granularity | string | no | "day" | "month"; the two grains the rollups store. |
meter | string | no | Optional meter id filter; empty means every meter in the bucket. |
Response body
| Field | Type | Description |
|---|---|---|
rows | array of UsageRow | One row per bucket in the window that was asked for. |
frozenThrough | string (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
GET /v1/project/usage?from=1772323200000&to=1774915200000&granularity=month
Example response
{
"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.
| Field | Type | Required | Description |
|---|---|---|---|
from | string (64-bit integer) | yes | The start of the range, Unix ms, inclusive, on the slice's own instant: endedAt for session, day for day, goneAt for client. Required. |
to | string (64-bit integer) | yes | The 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. |
slice | string | no | Which 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. |
subject | string | no | Keep 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. |
cursor | string | no | Where 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. |
limit | number | no | How 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. |
format | string | no | json (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
| Field | Type | Description |
|---|---|---|
sessions | array of SessionUsage | The session or day records of this page, in the slice's instant order and then by session. Answered for slice session and day. |
clients | array of ClientUsage | The client records of this page, by goneAt and then by session and client. Answered for slice client. |
nextCursor | string | Where 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. |
frozenThrough | string (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
GET /v1/project/ledger?from=1772323200000&to=1774915200000&slice=session&subject=tenant_42&limit=1
Example response
{
"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
| Field | Type | Description |
|---|---|---|
token | string | bsk_cs_..., presented as Authorization: Bearer. |
expiresAt | string (64-bit integer) | Unix ms; identity.consoleTokenTtlS after issue. |
userId | string | The signed-in person's id. |
mfaAt | string (64-bit integer) | Unix ms this user last passed MFA; 0 when they never have. |
Example response
{
"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
| Field | Type | Description |
|---|---|---|
userId | string | The signed-in person's id. |
email | string | Absent until a console user record carries an address; the console sign-in alone does not. |
mfaAt | string (64-bit integer) | Unix ms this user last passed MFA; 0 when they never have. |
organizations | array of OrganizationMembership | Every organization this person holds a relation in, with the highest relation they hold in each. |
mfaGraceEndsAt | string (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. |
projects | array of ProjectMembership | Every project this person holds a relation on, directly or through their organization, with each project relation they hold there. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
orgName | string | no | Empty takes the default name. |
projectName | string | no | Empty takes the default name. |
Response body
| Field | Type | Description |
|---|---|---|
orgId | string | The organization the claim created. |
projectId | string | The first project in it. |
Example request
{
"orgName": "Northwind",
"projectName": "Lobby"
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
email | string | no | The address to mail the sign-in link to. |
Response body
None. The service answers 204.
Example request
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
token | string | no | The token out of the mailed link. It is good once. |
idToken | string | no | The 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
| Field | Type | Description |
|---|---|---|
token | string | bsk_cs_..., presented as Authorization: Bearer. |
expiresAt | string (64-bit integer) | Unix ms; identity.consoleTokenTtlS after issue. |
userId | string | The signed-in person's id. |
mfaAt | string (64-bit integer) | Unix ms this user last passed MFA; 0 when they never have. |
Example request
{
"token": "5m8qTzX..."
}
Example response
{
"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
| Field | Type | Description |
|---|---|---|
url | string | The provider's authorization URL, carrying the state and the PKCE S256 challenge. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
idToken | string | no | The Identity Platform ID token the popup answered. |
providerIdToken | string | no | The provider's own ID token the popup answered (credential.idToken). |
code | string | no | The authorization code, on the redirect shape only; empty on the popup shape. |
state | string | no | The state the authorization URL carried, on the redirect shape only; it must equal the state cookie's. |
Response body
| Field | Type | Description |
|---|---|---|
token | string | bsk_cs_..., presented as Authorization: Bearer. |
expiresAt | string (64-bit integer) | Unix ms; identity.consoleTokenTtlS after issue. |
userId | string | The signed-in person's id. |
mfaAt | string (64-bit integer) | Unix ms this user last passed MFA; 0 when they never have. |
Example request
{
"idToken": "eyJhbGciOi...",
"providerIdToken": "eyJhbGciOi..."
}
Example response
{
"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
| Field | Type | Description |
|---|---|---|
providers | array of SignInProvider | One row per federated provider of the private MVP, google then microsoft. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
idToken | string | no | The Identity Platform ID token the console holds after passing the factor again with the web SDK. |
Response body
| Field | Type | Description |
|---|---|---|
token | string | bsk_cs_..., presented as Authorization: Bearer. |
expiresAt | string (64-bit integer) | Unix ms; identity.consoleTokenTtlS after issue. |
userId | string | The signed-in person's id. |
mfaAt | string (64-bit integer) | Unix ms this user last passed MFA; 0 when they never have. |
Example request
{
"idToken": "eyJhbGciOi..."
}
Example response
{
"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
| Field | Type | Description |
|---|---|---|
deviceCode | string | The secret the terminal polls with. |
userCode | string | The short code the human compares against the one on the approval page. |
verificationUri | string | The console page to open, with userCode already in it: one URL on one line, so the person has no second address to compare. |
expiresIn | number | Seconds this code is good for: identity.deviceCodeTtlS = 600. |
interval | number | Seconds between polls: identity.devicePollIntervalS = 5. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
deviceCode | string | no | The deviceCode of the answer above, sent back unchanged. |
Response body
| Field | Type | Description |
|---|---|---|
token | string | bsk_cs_..., presented as Authorization: Bearer. |
expiresAt | string (64-bit integer) | Unix ms; identity.consoleTokenTtlS after issue. |
userId | string | The signed-in person's id. |
mfaAt | string (64-bit integer) | Unix ms this user last passed MFA; 0 when they never have. |
Example request
{
"deviceCode": "kR3v8Yq..."
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
userCode | string | no | The short code the approval page is showing, which the person compared against the one in the terminal. |
decision | string | no | approve or deny, and nothing else: any other word is a validation refusal naming decision. |
Response body
None. The service answers 204.
Example request
{
"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
| Field | Type | Description |
|---|---|---|
expiresIn | number | Seconds until the pending device code expires (RFC 8628, CALL-1192); an approved or spent code answers invalid_key instead. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
orgId | string | yes | The organization the console user is a member of, as org_.... |
email | string | yes | The console user's address. |
Response body
None. The service answers 204.
Example request
{
"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
| Field | Type | Description |
|---|---|---|
orgId | string | The organization's id. |
name | string | The organization's name, as it is shown in the console. |
createdAt | string (64-bit integer) | Unix ms. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | The organization's new name, 1-64 characters after trimming. |
Response body
The OrgView object, written out under Objects.
Example request
{
"name": "Northwind Games"
}
Example response
{
"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
| Field | Type | Description |
|---|---|---|
projects | array of ProjectView | One project per row. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Your own name for the project, 1 to 64 characters. The slug is made from it. |
slug | string | no | The public handle to claim. |
Response body
The ProjectView object, written out under Objects.
Example request
{
"name": "Acme demo",
"slug": "acme-demo"
}
Example response
{
"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
| Field | Type | Description |
|---|---|---|
keys | array of KeyView | One organization key per row. A key's full string is never listed. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
label | string | no | A name for the key, shown wherever the key is listed. |
projects | array of string | no | Empty means every project in the organization. |
expiresAt | string (64-bit integer) | no | When the key expires, Unix ms. |
permissions | array of string | yes | The permission list the organization key holds, from the one vocabulary: the list is the fence the role used to stand in for. |
subjects | array of string | no | An organization key holding tokens:mint only, validated as CreateKeyRequest.subjects is: the subject prefixes a mint under it may name, as display:. |
login | boolean | no | True 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
{
"label": "console",
"projects": [
"proj_g1qr384an9asb4kdp10e"
],
"permissions": [
"sessions:open",
"project:read"
]
}
Example response
{
"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.
| Parameter | Type | Required | Description |
|---|---|---|---|
reason | string | no | "manual" (the default) | "compromise". Anything else is refused validation with details.field reason. |
Response body
The KeyView object, written out under Objects.
Example response
{
"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
| Field | Type | Description |
|---|---|---|
orgs | array of OrgView | The organizations the signed-in person is a member of, newest first. |
staff | boolean | True 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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | no | The name to give the organization. |
plan | PlanRequest | no | The 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
{
"name": "Northwind"
}
Example response
{
"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
| Field | Type | Description |
|---|---|---|
orgId | string | The organization the plan is on. |
plan | OrgPlan | The plan on the organization's record: its name, its ceilings, and when it expires. |
expired | boolean | Whether the plan has passed its expiry, read against the clock when the request was answered. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | The named plan to set. Its template resolves into values on the organization's record. |
ceilings | object, string keys to string (64-bit integer) | no | Overrides on the template, keyed by the catalogue parameter (concurrentSessions), the one spelling on every wire surface; only rows a project may raise. |
expiresAt | string (64-bit integer) | no | Unix ms. |
Response body
| Field | Type | Description |
|---|---|---|
orgId | string | The organization the plan is on. |
plan | OrgPlan | The plan on the organization's record: its name, its ceilings, and when it expires. |
expired | boolean | Whether the plan has passed its expiry, read against the clock when the request was answered. |
Example request
{
"name": "experimental",
"ceilings": {
"concurrentSessions": "5"
},
"expiresAt": "1776592000000"
}
Example response
{
"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
| Field | Type | Description |
|---|---|---|
orgId | string | The organization in the path, as org_.... |
mfaRequired | boolean | Whether every member must enrol a second factor; false by default. |
staffAccess | string | allowed (the default) or refused. |
orgKeyMinting | string | keys (the default) or console. |
keyMaxLifetime | string | The longest an organization key may live, as a duration; absent when none is set. |
deviceSignIn | string | allowed (the default) or refused. |
providerSecondFactor | string | ignore (the default) or accept: whether a provider's second factor counts in place of Bitskiff's. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
mfaRequired | boolean | no | Whether every member must enrol a second factor. |
staffAccess | string | yes | Whether platform staff may read the organization's records: allowed or refused. |
orgKeyMinting | string | no | Who makes organization keys: keys (a key holding keys:manage, or a console sign-in) or console (a console sign-in only). |
keyMaxLifetime | string | no | The longest an organization key made or rotated from now on may live, as a duration (2160h), at most a year; empty clears it. |
deviceSignIn | string | no | Whether a console sign-in a device approval issued (bitskiff login, bitskiff init) acts in this organization: allowed or refused. |
providerSecondFactor | string | no | Whether 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
| Field | Type | Description |
|---|---|---|
orgId | string | The organization in the path, as org_.... |
mfaRequired | boolean | Whether every member must enrol a second factor; false by default. |
staffAccess | string | allowed (the default) or refused. |
orgKeyMinting | string | keys (the default) or console. |
keyMaxLifetime | string | The longest an organization key may live, as a duration; absent when none is set. |
deviceSignIn | string | allowed (the default) or refused. |
providerSecondFactor | string | ignore (the default) or accept: whether a provider's second factor counts in place of Bitskiff's. |
Example request
{
"mfaRequired": true,
"staffAccess": "refused"
}
Example response
{
"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.
| Parameter | Type | Required | Description |
|---|---|---|---|
cursor | string | no | Where 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. |
limit | number | no | How 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
| Field | Type | Description |
|---|---|---|
orgs | array of PlatformOrgView | This page of organizations, ordered by id. |
nextCursor | string | Empty when this page is the last. |
committedClientSlots | number | The client slots every live plan commits, summed over every organization and not only this page. |
unboundedLivePlans | number | Live plans that bound no clients, so commit no counted slots. |
platformSlots | number | The platform ceiling's slots, the number committedClientSlots is read against. |
refusedOrgs | number | Organizations whose owner refused staff access: counted over every organization, never listed on any page. |
Example response
{
"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.
| Parameter | Type | Required | Description |
|---|---|---|---|
month | string | no | The UTC month to read, as YYYY-MM. Omit it for the current month. Anything else is refused validation with details.field month. |
Response body
| Field | Type | Description |
|---|---|---|
orgId | string | The organization, as org_.... |
month | string | The UTC month read, as YYYY-MM. |
projectCount | number | How many projects were summed. |
legs | array of PlatformCostLeg | One row per leg, in its order. |
total | number | The sum of every leg's cost whose inTotal is set, in dollars. |
totalKind | string | What the total is: an estimate whenever any leg in it is one. |
frozenThrough | string (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. |
floor | string | The line on the fixed floor, which the environment carries. |
Example response
{
"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
| Field | Type | Description |
|---|---|---|
members | array of MemberView | One row per member, owners first and then by address. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
role | string | yes | org_owner, org_admin, billing_admin or org_member. |
Response body
The MemberView object, written out under Objects.
Example request
{
"role": "org_admin"
}
Example response
{
"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
{
"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
| Field | Type | Description |
|---|---|---|
billingAccounts | array of BillingAccountView | The default first, then by creation. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | 1-64 characters, not unique. |
legalName | string | no | The billed party's legal name. |
billingEmail | string | no | The address invoices are sent to. |
address | string | no | The billed party's postal address, free text. |
taxId | string | no | The billed party's tax identifier, free text. |
Response body
The BillingAccountView object, written out under Objects.
Example request
{
"name": "Agency",
"legalName": "Agency Ltd",
"billingEmail": "accounts@agency.example"
}
Example response
{
"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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
name | string | no | 1-64 characters. |
isDefault | boolean | no | true 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. |
legalName | string | no | An empty string clears it. |
billingEmail | string | no | An empty string clears it. |
address | string | no | An empty string clears it. |
taxId | string | no | An empty string clears it. |
Response body
The BillingAccountView object, written out under Objects.
Example request
{
"isDefault": true,
"taxId": "GB123456789"
}
Example response
{
"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.
| Parameter | Type | Required | Description |
|---|---|---|---|
month | string | no | The UTC month to read, as YYYY-MM. Omit it for the current month. Anything else is refused validation with details.field month. |
Response body
| Field | Type | Description |
|---|---|---|
billingAccountId | string | The billing account rolled up. |
orgId | string | Its organization. |
month | string | The UTC month read, as YYYY-MM. |
lines | array of BillingUsageLine | One 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. |
totals | array of BillingMeterTotal | One row per owned meter, in the same order. |
frozenThrough | string (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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
billingAccountId | string | yes | A bill_ id of the project's own organization. |
Response body
| Field | Type | Description |
|---|---|---|
projectId | string | The project, as proj_.... |
billingAccountId | string | The billing account it is attached to now. |
previousBillingAccountId | string | The one it was attached to before; empty when it had none. |
Example request
{
"billingAccountId": "bill_8wcd7g4kq2xm9pbz3tvn"
}
Example response
{
"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.
| Parameter | Type | Required | Description |
|---|---|---|---|
cursor | string | no | Where 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. |
limit | number | no | How 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
| Field | Type | Description |
|---|---|---|
invoices | array of InvoiceView | Headers: lines is empty on each. |
nextCursor | string | Empty when this page is the last. |
Example response
{
"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
{
"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
| Field | Type | Description |
|---|---|---|
url | string | The https URL; empty when none is set. |
secret | string | Hex, returned by the PUT that derives it and always empty on the GET; the value a receiver recomputes the signature with. |
kid | string | The name of the signing key version, as X-Bitskiff-Signature-Kid carries it. |
kinds | array of string | The invoice kinds the endpoint subscribes to: invoice.issued, invoice.paid and invoice.voided, each opt-in; empty is none. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
url | string | no | The https URL deliveries go to, checked against the webhook URL policy. |
kinds | StringListPatch | no | Absent leaves the subscription as it was; present replaces it, and present with no values clears it. |
Response body
| Field | Type | Description |
|---|---|---|
url | string | The https URL; empty when none is set. |
secret | string | Hex, returned by the PUT that derives it and always empty on the GET; the value a receiver recomputes the signature with. |
kid | string | The name of the signing key version, as X-Bitskiff-Signature-Kid carries it. |
kinds | array of string | The invoice kinds the endpoint subscribes to: invoice.issued, invoice.paid and invoice.voided, each opt-in; empty is none. |
Example request
{
"url": "https://example.com/hooks/billing",
"kinds": {
"values": [
"invoice.issued",
"invoice.paid",
"invoice.voided"
]
}
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
month | string | no | The UTC month, YYYY-MM, which must have ended; empty is the month before the current one. |
Response body
| Field | Type | Description |
|---|---|---|
invoices | array of InvoiceView | Headers: lines is empty on each. |
nextCursor | string | Empty when this page is the last. |
Example request
{
"month": "2026-09"
}
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
status | string | yes | paid or void. |
reason | string | yes | 1-200 characters. |
Response body
The InvoiceView object, written out under Objects.
Example request
{
"status": "void",
"reason": "issued for a walk-through"
}
Example response
{
"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
| Field | Type | Description |
|---|---|---|
invitations | array of InvitationView | One invitation per row, newest first. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
email | string | yes | The address to invite. |
roles | array of string | yes | The roles accepting the invitation writes. |
projectId | string | no | Empty unless a role in roles is a project role. |
Response body
The InvitationView object, written out under Objects.
Example request
{
"email": "dev@example.com",
"roles": [
"org_member"
]
}
Example response
{
"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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
token | string | no | The token out of the invitation mail. It is good once. |
Response body
| Field | Type | Description |
|---|---|---|
orgId | string | The organization that was joined. |
orgName | string | Its name. |
roles | array of string | The relations the invitation carried, now held by the person who accepted it. |
Example request
{
"token": "q7Rk2mWv..."
}
Example response
{
"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
| Field | Type | Description |
|---|---|---|
invitations | array of InvitationView | One invitation per row, newest first. |
Example response
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
email | string | yes | The address to invite to the platform. |
plan | PlanRequest | yes | The 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
{
"email": "dev@example.com",
"plan": {
"name": "experimental",
"expiresAt": "1776592000000"
}
}
Example response
{
"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
{
"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.
| Field | Type | Required | Description |
|---|---|---|---|
cursor | string | no | Where 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. |
limit | number | no | How 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
| Field | Type | Description |
|---|---|---|
events | array of ProjectEvent | The events, oldest first, as far as this page goes. |
nextCursor | string | Where 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. |
trimmed | boolean | True 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
GET /v1/project/events?limit=2
Example response
{
"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
| Field | Type | Description |
|---|---|---|
url | string | Where deliveries are posted. |
secret | string | Hex, returned by the PUT that derives it and always empty on the GET; the value a receiver recomputes the MAC with. |
kid | string | Which signing secret a delivery was signed with. It travels on the X-Bitskiff-Signature-Kid header, so a rotation is readable at the receiver. |
kinds | array of string | The 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
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
url | string | no | A new URL for deliveries. The empty string clears the webhook. |
kinds | StringListPatch | no | Absent leaves the subscription as it was; present replaces it, and present with no values clears it. |
Response body
| Field | Type | Description |
|---|---|---|
url | string | Where deliveries are posted. |
secret | string | Hex, returned by the PUT that derives it and always empty on the GET; the value a receiver recomputes the MAC with. |
kid | string | Which signing secret a delivery was signed with. It travels on the X-Bitskiff-Signature-Kid header, so a rotation is readable at the receiver. |
kinds | array of string | The 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
{
"url": "https://example.com/hooks/bitskiff"
}
Example response
{
"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
| Field | Type | Description |
|---|---|---|
rows | array of SdkRow | One row per package, version and protocol version. |
projectId | string | The project the credential named. |
Example response
{
"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
| Field | Type | Description |
|---|---|---|
orgId | string | The organization in the path. |
rows | array of SdkRow | One row per package, version and protocol version. |
Example response
{
"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
| Field | Type | Description |
|---|---|---|
releases | array of PlatformSdkRelease | Ordered by package, then version, then protocol version. |
Example response
{
"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
| Field | Type | Description |
|---|---|---|
version | string | The release, X.Y.Z. |
digests | array of ReleaseDigest | One per SDK tarball, ordered by package. |
recordedAt | string (64-bit integer) | Unix ms the cut recorded them. |
Example response
{
"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.
| Field | Type | Required | Description |
|---|---|---|---|
action | string | no | Keep only the rows with this action, such as key.created. Omit it for every action. |
actor | string | no | Keep only the rows this actor wrote. Omit it for every actor. |
since | string (64-bit integer) | no | Ms since epoch, inclusive. |
until | string (64-bit integer) | no | Ms since epoch, exclusive. |
cursor | string | no | Where 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. |
limit | number | no | How 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
| Field | Type | Description |
|---|---|---|
events | array of AuditEvent | One audited change per row. |
nextCursor | string | Where 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
GET /v1/project/audit?action=key.created&limit=20
Example response
{
"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
| Field | Type | Description |
|---|---|---|
engineUrl | string | Where 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. |
region | string | The region that answered this call. |
env | string | Which environment this endpoint belongs to: local, dev or prod. |
apiId | string | The sender identity the service itself signs with in this region, a_<region>: the same value a ticket carries, offered here for support. |
Example response
{
"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
| Field | Type | Description |
|---|---|---|
versions | array of ProtocolVersion | One row per protocol version the service still speaks. |
Example response
{
"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
| Field | Type | Description |
|---|---|---|
room | string | The engine room this run is measured in. It is made for this answer, holds two participants at most, and finishes once it empties. |
signalingUrl | string | Where both ends dial the engine, the same address GET /v1/whoami answers with. |
sender | BenchParticipant | The end that sends. Both ends are the same browser: this one publishes the run's frames. |
receiver | BenchParticipant | The end that receives, and where delivery is measured. |
expiresIn | number | Seconds 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
{
"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:
counter = floor(unixMillis / 1000 / step), wherestepis the session'sjoinCodeRotation:30sby default,15sto2m, and the counter uses its whole seconds.mac = HMAC-SHA256(secret, counter as 8 big-endian bytes), wheresecretis the material'ssecretdecoded from base64 (32 bytes).- Dynamic truncation, extended to five bytes:
offset = mac[31] & 0x0f, then take the five bytes atoffsetas a big-endian number and keep its low 35 bits. - Render that number base 62 over the digits
0-9A-Za-z(Ais 10,ais 36), and left-pad it with0to 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 path | Who may call it | What it does |
|---|---|---|
GET /v1/project/deadlines/parked | An 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}/unpark | An 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}/digests | An 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/notices | An 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.
| Code | HTTP | Retryable | Clears | Meaning |
|---|---|---|---|---|
validation | 400 | no | never | The request or the session config is malformed; details.field names the field, details.header the header. |
quota_above_ceiling | 400 | no | never | A 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_required | 400 | no | never | The session requires end-to-end encryption and this join carries nothing to do it with. |
invalid_key | 401 | no | never | The 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_token | 401 | no | never | The token is not usable; details.reason is expired, revoked or unknown, and the answer is a fresh token from your own backend. |
handoff_invalid | 401 | no | never | The 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_allowed | 403 | no | never | A publishable key without an allowed Origin. |
restricted_mode | 403 | no | never | A publishable-key open on a restricted project. |
plan_expired | 403 | no | never | The 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_browser | 403 | no | never | A secret or organization key presented with any Origin header; audited. |
forbidden | 403 | no | never | The credential may not call this route, does not own this session, or lacks a permission it needs; details.reason says which. |
wrong_client | 403 | no | never | This 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_found | 404 | no | never | Nothing 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_found | 404 | no | never | No 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_code | 404 | no | never | The join code did not decode, or its rotating part did not verify; counted against the bad-code limit. |
clients_full | 409 | no | waiting | The session is at maxClients. |
session_ended | 409 | no | never | The session has already ended; a fresh open is the way on. |
host_released | 409 | no | waiting | The 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_rotating | 409 | no | waiting | The key is inside its rotation overlap and cannot take this change. |
join_url_missing | 409 | no | never | The project has no joinUrl to point a client at; details.field is joinUrl. |
replaced | 409 | no | never | A 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_off | 409 | no | never | A 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_pending | 409 | yes | waiting | A 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_mismatch | 403 | no | never | On 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_mismatch | 403 | no | never | On 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_flight | 409 | yes | waiting | An open, or a handoff redeem, with this Idempotency-Key is still running; retry after retryAfterMs for its answer. |
seat_spent | 410 | no | never | This client's budget is spent; rejoin with a code. |
host_resume_expired | 410 | no | never | The host's resume window is spent; open a fresh session. |
protocol_unsupported | 426 | no | never | The service does not speak the protocol version this caller asked for; details.supported lists the ones it does. |
rate_limited | 429 | yes | waiting | Over a rate limit, per key, token, IP or session; retryAfterMs set. |
concurrency_limit | 429 | yes | waiting | A 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_exceeded | 429 | no | waiting | A usage cap or the spend cap is spent; retryAfterMs is the time to reset. |
turn_quota_exceeded | 429 | no | waiting | The 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. |
internal | 500 | no | waiting | Something inside the service failed; requestId is the handle support asks for. |
relay_not_configured | 503 | no | never | This 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. |
unavailable | 503 | yes | waiting | The service or a dependency is down; retryAfterMs set. |
overloaded | 503 | yes | waiting | The platform is at its admission ceiling; the open or join was refused and nothing live was touched; retryAfterMs set. |
network | - | yes | waiting | The request never reached the service. |
timeout | - | yes | waiting | The request or the handshake did not finish inside its budget. |
protocol | - | no | never | A response or an envelope did not match the pinned protocol. |
engine | - | no | waiting | The transport or the engine SDK failed; cause carries its error, never rethrown. |
disposed | - | no | never | The handle was disposed before the call could finish. |
listener_error | - | no | never | A listener the app bound with on threw; details.event names the event and cause carries the throw. |
sdk_internal | - | no | never | Something 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 | - | no | never | This 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.
| Field | Type | Description |
|---|---|---|
type | string | "user" | "admin_key" | "secret_key" | "token" | "system". |
id | string | The 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.
| Field | Type | Description |
|---|---|---|
id | string | The row's own id. |
at | string (64-bit integer) | When it happened, Unix ms. |
orgId | string | The organization it happened in. |
projectId | string | The project it happened in, when it was a project's change. |
actor | AuditActor | Who did it. |
action | string | What was done, such as key.created. |
target | AuditTarget | What it was done to. |
before | object, string keys to string | The fields that changed, as they were. |
after | object, string keys to string | The same fields, as they are now. |
requestId | string | The request that made the change: the handle support traces it by. |
ip | string | The address the request came from. |
AuditTarget
What a change was made to, by its kind and its id.
| Field | Type | Description |
|---|---|---|
type | string | "key" | "project" | "session". |
id | string | The id of what was changed. |
BenchParticipant
One end of a benchmark run.
| Field | Type | Description |
|---|---|---|
ticket | string | What 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.
| Field | Type | Description |
|---|---|---|
billingAccountId | string | The bill_ id. |
orgId | string | The organization it belongs to, as org_.... |
name | string | The organization's own word for it, 1-64 characters. |
isDefault | boolean | True on the organization's default billing account, the one the monthly fee lands on; exactly one billing account of an organization is the default. |
legalName | string | The billed party's legal name, as an invoice prints it; absent until entered. |
billingEmail | string | The address invoices are sent to; absent until entered. |
address | string | The billed party's postal address, free text; absent until entered. |
taxId | string | The billed party's tax identifier, free text; absent until entered. |
createdAt | string (64-bit integer) | Unix ms it was created. |
projectIds | array of string | The proj_ ids of the projects attached to it now. |
BillingMeterTotal
One owned meter summed over every project of the roll-up.
| Field | Type | Description |
|---|---|---|
meter | string | As BillingUsageLine.meter. |
leg | string | As BillingUsageLine.leg. |
quantity | number | The sum of that meter's lines. |
unit | string | As BillingUsageLine.unit. |
BillingUsageLine
One project's owned meter over the month: the line an invoice will carry.
| Field | Type | Description |
|---|---|---|
projectId | string | The project, as proj_.... |
meter | string | The meter: client_minutes, session_minutes, relay_bytes or data_bytes_sent, the four billable legs. |
leg | string | The leg the meter is, as that table names it. |
quantity | number | The month's quantity over the days the project was attached here. |
unit | string | client minutes, session minutes or bytes. |
CapacityLimit
One cap that is not ok.
| Field | Type | Description |
|---|---|---|
limit | string | The parameter spelling details.limit carries (clientMinutesPerDay). |
scope | string | What 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. |
scopeId | string | The same id a refusal and a limit.* webhook carry. |
status | string | One of CAPACITY_STATUS_OK, CAPACITY_STATUS_WARNING, CAPACITY_STATUS_EXCEEDED. |
value | string (64-bit integer) | What the period has spent, in the row's own unit. |
ceiling | string (64-bit integer) | The cap it is spending against. |
resetAt | string (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).
| Field | Type | Description |
|---|---|---|
status | string | The worst row's; OK over an empty list. One of CAPACITY_STATUS_OK, CAPACITY_STATUS_WARNING, CAPACITY_STATUS_EXCEEDED. |
limits | array of CapacityLimit | One row per cap that is not OK; more than one stands at once. |
at | string (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.
| Field | Type | Description |
|---|---|---|
channelId | number | Allocated from 1 upward in declaration order, unique for the life of the session and never reused inside it. |
name | string | Non-empty, at most 64 UTF-8 bytes; checked at declaration. |
delivery | string | One of DELIVERY_RELIABLE, DELIVERY_LOSSY. |
ordered | boolean | Handed over in send order; true on reliable, false on lossy by default. |
lifetimeMs | number | 0 = unset. |
maxHz | number | The most messages a second one sender sends on this channel. |
maxBytes | number | The largest one message on this channel, sent or accepted. |
latestOnly | boolean | Lossy only: drop a received packet behind the latest delivered. |
linkMaxHz | number | The 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.
| Field | Type | Description |
|---|---|---|
platform | string | The os-family registry: "android" | "ios" | "windows" | "macos" | "linux" | "other". |
runtime | string | The client-runtime registry: "browser" | "webview" | "native". |
sdkName | string | The client SDK's package name, such as "ts-client": the first half of Hello.sdk. |
sdkVersion | string | The client SDK's semver, such as "0.2.3": the second half of Hello.sdk, split so a roster can show the version alone. |
transport | string | The 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.
| Field | Type | Description |
|---|---|---|
id | string | The 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. |
sessionId | string | The session the client was in. |
clientId | string | The client's id in that session. |
keyId | string | The client key the client joined with. Empty for the sample client page. |
sessionKeyId | string | The key that opened the session, the session record's keyId. |
subject | string | The subject of the client's own join token. Empty for an anonymous client. |
label | string | The host's label for the session. |
joinedAt | string (64-bit integer) | When the client joined, Unix ms. |
goneAt | string (64-bit integer) | When the client's place was released, Unix ms. |
goneReason | string | Why the client is gone, a gone reason. |
bundle | string | The bundle the client held when it left. Price a promoted client on msByBundle, not on this. |
seatMs | string (64-bit integer) | The client's time in milliseconds: connected, away or disconnected. Joining is not counted. |
msByBundle | object, string keys to string (64-bit integer) | Time under each bundle the client held, joining included, so it sums to its whole time. |
resumed | boolean | Whether the client reconnected into its place inside its budget. |
bytesReported | string (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. |
writtenAt | string (64-bit integer) | When the ledger last wrote this record, Unix ms. Absent on the webhook. |
writerRevision | number | Which revision of the ledger's writer wrote the record. Absent on the webhook. |
subjectUnknown | boolean | True 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. |
messagesReported | string (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. |
dataMessagesUp | string (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. |
dataMessagesDown | string (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. |
dataBytesUp | string (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. |
dataBytesDown | string (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.
| Field | Type | Description |
|---|---|---|
kind | string | The frame's kind, such as hello, presence, ping, heartbeat or transport_setup. |
count | string (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.
| Field | Type | Description |
|---|---|---|
id | string | The opaque id a host names at open; it admits nobody. |
label | string | The developer's own text for the NFC tag or sticker that carries it. |
code | string | The 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. |
sessionId | string | The open session holding the claim; empty when none does. |
createdAtMs | string (64-bit integer) | Unix ms. |
replacedAtMs | string (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.
| Field | Type | Description |
|---|---|---|
transport | string | The class: "peer", "relay", "server" or a declared transport's name. |
grade | string | Its last grade, "good", "fair" or "poor"; empty when none was read. |
rttMs | number | Its last round trip time reading, in ms; 0 when none was read. |
noDirectPath | boolean | The page found no direct path from where it stood. |
HandoffSuccessor
The successor of a handoff under way, as a host reads it.
| Field | Type | Description |
|---|---|---|
kind | string | The successor's kind of process: "page" | "app". |
phase | string | One of HANDOFF_PHASE_PENDING, HANDOFF_PHASE_REDEEMED, HANDOFF_PHASE_DONE, HANDOFF_PHASE_FAILED. |
deadlineMs | string (64-bit integer) | Unix ms on the service's clock the handoff window ends. |
hint | HandoffHint | The 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.
| Field | Type | Description |
|---|---|---|
invitationId | string | The invitation's id. |
orgId | string | The organization the invitation is into. |
email | string | The address that was invited. |
roles | array of string | The roles accepting it writes. |
projectId | string | Empty unless a role is a project role. |
invitedBy | string | The user_ id of the console user who sent it. |
createdAt | string (64-bit integer) | Unix ms. |
expiresAt | string (64-bit integer) | Unix ms. |
acceptedAt | string (64-bit integer) | Unix ms; 0 while it is not accepted. |
revokedAt | string (64-bit integer) | Unix ms; 0 while it is not revoked. |
state | string | "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. |
kind | string | "organization" (into an existing organization) or "platform" (creates a new one on acceptance). |
plan | PlanRequest | The plan the organization created on acceptance will carry. Set only on a platform invitation. |
InvoiceLineView
One line item of an invoice.
| Field | Type | Description |
|---|---|---|
lineNo | number | 1-based, in the order the invoice prints its lines. |
kind | string | usage (one project and one owned meter over the month), fee (the monthly fee parameter, on the organization's default billing account), credit or adjustment. |
projectId | string | The project a usage line is for, as proj_...; empty on the fee line. |
meter | string | client_minutes, session_minutes, relay_bytes or data_bytes_sent on a usage line; fee_monthly on the fee line. |
leg | string | What the line is, as and 11 name it. |
quantity | number | The month's quantity over the days the project was attached here. |
unit | string | client minutes, session minutes, bytes or month. |
unitPriceMicros | string (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. |
amountMicros | string (64-bit integer) | The line's amount, in millionths of currency, rounded to the cent. |
reason | string | Why 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.
| Field | Type | Description |
|---|---|---|
invoiceId | string | The invc_ id. |
billingAccountId | string | The billing account it is addressed to. |
orgId | string | The billed organization. |
period | string | The UTC month it covers, as YYYY-MM. |
periodStart | string (64-bit integer) | Unix ms the month starts, inclusive. |
periodEnd | string (64-bit integer) | Unix ms the next month starts, exclusive. |
status | string | issued, paid or void; a zero total is issued already paid, and a void keeps its number. |
number | string | The 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. |
rateCardVersion | number | The version of the rate card it was rated on. |
currency | string | ISO 4217, as USD. |
subtotalMicros | string (64-bit integer) | The sum of the lines, in millionths of currency. |
totalMicros | string (64-bit integer) | What is owed, in millionths of currency. |
lines | array of InvoiceLineView | Every line item on a read of one invoice; empty on a list, which carries headers only. |
accountName | string | The billing account's name when it was issued. |
legalName | string | The billed party's legal name when it was issued; absent when none was entered. |
billingEmail | string | The address invoices were sent to when it was issued; absent when none was entered. |
address | string | The billed party's postal address when it was issued; absent when none was entered. |
taxId | string | The billed party's tax identifier when it was issued; absent when none was entered. |
issuedAt | string (64-bit integer) | Unix ms it was issued. |
paidAt | string (64-bit integer) | Unix ms it was marked paid; 0 while it is not. |
voidedAt | string (64-bit integer) | Unix ms it was voided; 0 while it is not. |
statusReason | string | Why it is paid or void: zero_total on an invoice issued already paid, else the reason staff gave; empty while it is issued. |
pdfSha256 | string | The hex SHA-256 of the archived PDF. |
frozenThrough | string (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.
| Field | Type | Description |
|---|---|---|
version | string | "1". |
regionHint | string | One char. |
sessionCode | string | 7 chars; the host builds the 15-character join code around it. |
secret | string (base64) | 32 bytes; HMAC key for the TOTP component. |
stepS | number | Config.joinCodeRotation, in the whole seconds the counter uses. |
joinUrl | string | The project's joinUrl with "#" appended; the SDK appends the code. |
JoinGrant
A join grant: opaque, session-bound, single-use, and holding no place.
| Field | Type | Description |
|---|---|---|
grant | string | The grant itself: base64url, unpadded, of 32 random bytes. |
expiresAtMs | string (64-bit integer) | Absolute Unix ms on the service's clock past which the grant no longer redeems. |
serverTimeMs | string (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.
| Field | Type | Description |
|---|---|---|
grant | string | As JoinGrant.grant. |
holder | string | Base64url, 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.
| Field | Type | Description |
|---|---|---|
holderHash | string | Base64url, 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.
| Field | Type | Description |
|---|---|---|
concurrentSessions | number | How many sessions this key may hold open at once. |
concurrentClients | number | How many clients this key's sessions may hold at once. |
sessionOpensPerMinute | number | How many sessions this key may open in a minute. |
clientMinutesPerDay | number | How many client minutes a day this key may spend. |
ticketMintsPerMinute | number | How many tickets this key may mint in a minute. |
tokensMaxTtl | string | The ceiling on a mint's ttl and on a live token's effective expiry, clamped min. |
requireToken | string | The ordered enum "off" < "token" < "token_bound_to_session": a key may require, never relax, and what it requires reaches that key's sessions only. |
joinsPerMinuteKey | number | joinsPerMinuteKey: joins one client key admits in a minute, across every session and address it joins from. |
bytesPerSecond | number | How 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. |
messagesPerSecond | number | How many messages a second each client joining with this key may send. It may sit under the project's value and never above it. |
linkMessagesPerSecond | number | The 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.
| Field | Type | Description |
|---|---|---|
keyId | string | The key's id, which is what every route names it by. It is not the key string. |
type | string | "publishable" | "secret" | "admin". |
orgId | string | The organization the key belongs to. |
projectId | string | Publishable and secret keys only. |
projects | array of string | Organization keys only; empty means every project in the organization. |
masked | string | The 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. |
secret | string | The one-time full string; empty otherwise. |
status | string | "active" | "rotating" | "revoked". |
label | string | Your own name for the key. |
origins | array of string | The origins a publishable key may be used from. |
limits | KeyLimits | The caps this key is held to. |
createdAt | string (64-bit integer) | When the key was created, Unix ms. |
createdBy | string | Who created it: the console user, or the key that minted it. |
lastUsedAt | string (64-bit integer) | When the key was last presented, Unix ms. Zero on a key that has never been used. |
retireAt | string (64-bit integer) | When a rotating key stops being accepted, Unix ms. Zero on a key that is not rotating. |
successorKeyId | string | The key that replaced this one in a rotation. |
revokedAt | string (64-bit integer) | When the key was revoked, Unix ms. Zero while it is live. |
revokedReason | string | "manual" | "rotation" | "compromise". |
permissions | array of string | The permission list the key holds: ["sessions:open"] on a host key, ["sessions:join"] on a client key. |
expiresAt | string (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). |
ancestry | array of string | The keys that made this one, root first, ending with its immediate maker. |
hostKeys | array of string | A client key's conditions.hostKeys: the host keys whose sessions it may join. |
subjects | array of string | A key holding tokens:mint: its conditions.subjects, the subject prefixes a mint under it may name. |
approvedBy | string | On 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.
| Field | Type | Description |
|---|---|---|
value | string (64-bit integer) | The cap itself. |
scope | string | Which 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. |
used | string (64-bit integer) | The current period's consumption; 0 for a rate limit, which has no period to spend. |
periodEnd | string (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.
| Field | Type | Description |
|---|---|---|
projectId | string | The project that asked, as proj_.... |
projectName | string | That project's name, for a person reading the list. |
limit | string | The limit in its parameter spelling (concurrentSessions), as a refusal's details.limit names it. |
value | string (64-bit integer) | The value asked for. |
current | string (64-bit integer) | The project's own value, which applies while the raise is pending. |
ceiling | string (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.
| Field | Type | Description |
|---|---|---|
node | string | The node's name, as the bye named it. |
boot | string | The 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.
| Field | Type | Description |
|---|---|---|
userId | string | The user_ id of the console user. |
email | string | The verified address the console user signs in with; empty when the store holds no record of them. |
role | string | The organization role the relations amount to, highest first: org_owner, org_admin, billing_admin or org_member. |
relations | array of string | The 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. |
joinedAt | string (64-bit integer) | Unix ms the first of those relations was written; 0 when the store kept no time. |
caller | boolean | True 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.
| Field | Type | Description |
|---|---|---|
name | string | The plan's name, such as experimental or unlimited. |
ceilings | object, 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. |
expiresAt | string (64-bit integer) | Unix ms; absent means it does not expire. |
setAt | string (64-bit integer) | Unix ms. |
setBy | string | Who 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.
| Field | Type | Description |
|---|---|---|
orgId | string | The organization's id. |
name | string | The organization's name. |
createdAt | string (64-bit integer) | Unix ms. |
OrganizationMembership
One organization the user holds a relation in, with the highest one they hold there.
| Field | Type | Description |
|---|---|---|
orgId | string | The organization's id. |
name | string | The organization's name. |
relation | string | The highest of "owner", "admin", "member". |
ParticipantHolder
The process that keeps a place's resume token.
| Field | Type | Description |
|---|---|---|
kind | string | The kind of process that keeps the place: "page" | "app". |
next | HandoffSuccessor | The 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.
| Field | Type | Description |
|---|---|---|
participantId | string | The client these meters are for, by its id on this wire. |
counters | object, string keys to string | That 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.
| Field | Type | Description |
|---|---|---|
participantId | string | The client these counters are for, by its id on this wire. |
stats | Stats | What 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.
| Field | Type | Description |
|---|---|---|
participantId | string | The client's id on this wire. |
grants | array of string | What 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. |
opener | boolean | True for the host, which holds the place the session was opened on. |
seatClass | string | Which 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. |
seat | number | Which 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. |
state | string | Where the client is: one of joining, connected, reconnecting, released, away or gone. |
presence | string | Whether the client is in front of the person using it or backgrounded. One of PRESENCE_PRESENT, PRESENCE_AWAY. |
joinedAt | string (64-bit integer) | When the client joined, Unix ms. |
sdk | string | Which SDK the client joined with and its version, such as ts-client/0.1.0. |
transportPath | string | The 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. |
bundle | string | The bundle this client joined with. One of BUNDLE_CONTROLLER, BUNDLE_VIEWER, BUNDLE_HOST. |
subject | string | The subject of the join token this client presented: the developer's own user id, straight off this client's own record. |
goneReason | string | Why this client is gone: a gone-surface reason of the reasons registry, straight off the client record. |
limits | SessionLimits | This client's own traffic caps: min(project row, the client key's own row), computed at join. |
connection | string | The record's connection, "reserved" | "attached" | "detached" | "released" | "gone". |
keyId | string | The id of the client key that admitted this client (key_...), never the key itself. |
origin | string | The 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. |
originSource | string | How 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. |
releasedTransport | string | The transport that carries a released client (connection "released"), as its host named it at the release; empty for every other connection. |
holder | ParticipantHolder | Who keeps this place: the holder's kind and, while a handoff is under way, its successor. |
entry | string | How this client's place was entered, one of these values: "code" (a rotating join code) or "fixed". |
keyThumbprint | string | The 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. |
grantsCeiling | array of string | The 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. |
environment | ClientEnvironment | The 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. |
metadata | object, string keys to string | The 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.
| Field | Type | Description |
|---|---|---|
name | string | The named plan to set. Its template resolves into values on the organization's record. |
ceilings | object, 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. |
expiresAt | string (64-bit integer) | Unix ms. |
PlatformCostLeg
One cost leg for one organization's month.
| Field | Type | Description |
|---|---|---|
leg | string | The leg's name as writes it. |
quantity | number | The month's quantity summed over the organization's projects, in unit. |
unit | string | The unit of quantity: a minute leg's minutes, GB, messages or requests. |
quantityKind | string | What the quantity is: measured, client reported, an estimate, or not metered per project. |
unitCost | number | What one unit costs Bitskiff in dollars. |
unitCostKind | string | What the unit cost is: an assumption, a parameter, free to Bitskiff, or none stated. |
cost | number | The product of quantity and unit cost, in dollars. |
costKind | string | What 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. |
inTotal | boolean | Whether cost is in the response's total. |
note | string | Why 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.
| Field | Type | Description |
|---|---|---|
orgId | string | The organization, as org_.... |
name | string | The organization's name. |
createdAt | string (64-bit integer) | Unix ms. |
ownerEmails | array of string | The address of every organization owner, sorted. |
plan | OrgPlan | The plan record as it stands. |
expired | boolean | Whether the plan has passed its expiry at answer time. |
projectCount | number | How many projects the organization holds. |
liveSessions | number | Live sessions across its projects, read from the gates. |
liveClients | number | Clients holding a place in those sessions, from the gates. |
clientSlots | number | The client slots the plan commits: its concurrentClients ceiling, counted at organization grain. |
platformSlots | number | The 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.
| Field | Type | Description |
|---|---|---|
release | SdkRow | The release, its sessions counted across every organization. |
orgIds | array of string | The organizations that ran it, ordered by id. |
refusedOrgs | number | Organizations that ran it and refused staff access: counted here, never named in orgIds. |
orgs | array of PlatformSdkReleaseOrg | The 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.
| Field | Type | Description |
|---|---|---|
orgId | string | The organization's id. |
owner | boolean | True 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.
| Field | Type | Description |
|---|---|---|
id | string | The event's own id, which is what a webhook delivery is deduplicated on and what this stream is paged by. |
type | string | Which kind of event this is, such as open.refused. |
at | string (64-bit integer) | When the event happened, Unix ms on the service's clock. |
data | string | The 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.
| Field | Type | Description |
|---|---|---|
projectId | string | The project's id. |
slug | string | The project's slug, as the console's address names it. |
orgId | string | The organization the project is in. |
relations | array of string | Each 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.
| Field | Type | Description |
|---|---|---|
projectId | string | The project's internal key. The slug is the handle you type. |
orgId | string | The organization the project belongs to. |
restrictedMode | boolean | True when this project takes no publishable-key opens: a session must be opened with a secret key or a token. |
joinUrl | string | The page a join code points a client at. Without one, an open is refused join_url_missing. |
e2ee | string | "off" | "required". |
name | string | The caller's own word for the project, 1-64 characters, not unique. |
createdAt | string (64-bit integer) | When the project was created, Unix ms. |
slug | string | The project's public handle: globally unique, immutable, lowercase letters, digits and hyphens, 5 to 30 characters. |
archivedAt | string (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.
| Field | Type | Description |
|---|---|---|
min | number | The oldest protocol version the caller can speak. |
max | number | The newest protocol version the caller can speak. |
ProtocolVersion
One protocol version the service still speaks, and when it stops.
| Field | Type | Description |
|---|---|---|
version | number | The protocol version. |
status | string | "current" | "supported" | "sunset". |
sunset | string (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.
| Field | Type | Description |
|---|---|---|
platform | string | android or ios. |
appId | string | The Android package name, or the iOS bundle id. |
fingerprints | array of string | Android: the SHA-256 signing certificate fingerprints, colon-separated upper-case hex, as assetlinks.json spells them. |
storeId | string | IOS only: the numeric App Store id the listing is built from. |
handoffLink | string | The https handoff link, on a host the app claims (assetlinks.json, apple-app-site-association) and the page is not served from. |
clientKeyId | string | The id (key_) of the publishable client key the app joins with. |
name | string | The app's name as the offer shows it: "Continue in the {name} app". |
gain | string | What 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.
| Field | Type | Description |
|---|---|---|
values | array of RegisteredApp | The 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.
| Field | Type | Description |
|---|---|---|
package | string | The package name, such as @bitskiff/host. |
file | string | The download's path under /downloads/, such as 0.2.2/bitskiff-host.tgz. |
sha256 | string | The lowercase hex sha256 of the tarball's bytes. |
bytes | string (64-bit integer) | The tarball's size in bytes. |
RelocatedClient
One client a re-placement is still waiting for.
| Field | Type | Description |
|---|---|---|
participantId | string | The client's client id. |
releasesAtMs | string (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.
| Field | Type | Description |
|---|---|---|
relocatedAtMs | string (64-bit integer) | Absolute Unix ms on the service's clock the session was last re-placed at (relocatedAt). |
clients | array of RelocatedClient | Each 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.
| Field | Type | Description |
|---|---|---|
code | string | rate_limited today: one of the two outstanding-grant ceilings is full. |
retryAfterMs | number | How long to wait before reserving again: until the soonest outstanding grant expires. |
details | object, string keys to string | limit (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.
| Field | Type | Description |
|---|---|---|
windowCount | number | Samples behind the window figures. |
windowP50Ms | number | Median round trip over the window, in milliseconds. |
windowP95Ms | number | 95th-percentile round trip over the window, in milliseconds. |
windowMaxMs | number | The slowest round trip in the window, in milliseconds. |
totalCount | string (64-bit integer) | How many round trips have been measured since this client joined. |
totalSumMs | string (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.
| Field | Type | Description |
|---|---|---|
used | string (64-bit integer) | Sessions counted for the life of the project. |
onboardingBound | number | Past this count every read of the project's settings warns, and nothing is refused—100. |
refusalBound | number | Past this count a join through the sample page is refused until joinUrl names a page of the project's own—1000. |
onSamplePage | boolean | Whether 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.
| Field | Type | Description |
|---|---|---|
sdk | string | The whole tag, <package>/<semver>, e.g. |
protocol | number | The protocol version the mints pinned. |
lastSeen | string (64-bit integer) | Unix ms of the latest mint that carried this release. |
sessions30d | string (64-bit integer) | Distinct sessions this release minted for in the last 30 days. |
package | string | The <package> half of the tag, e.g. |
version | string | The <semver> half, e.g. |
protocolStatus | string | That version's status in GET /v1/protocol: "current" | "supported" | "sunset". |
sunset | string (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.
| Field | Type | Description |
|---|---|---|
maxClients | number | Capacity 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. |
reconnectBudget | string | How long a client may spend reconnecting before its place is given up, as a duration string: 15s, 2m. |
backgroundBudget | string | How long a client may spend backgrounded before its place is given up, as a duration string: 5m, 30m. |
maxSessionTime | string | How long the session may run before the service ends it, as a duration string. 0s, the default, is no limit. |
idleTimeout | string | 0s = unlimited: the idle limit is off unless asked for. |
joinCodeRotation | string | How 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. |
hostResumeWindow | string | The 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. |
warningLead | string | Warning lead: how long before a terminal deadline each SDK raises its own warning. |
defaultGrants | array of string | The grants a new client starts with. One of GRANT_SEND_INPUT, GRANT_RECEIVE_INPUT, GRANT_BROADCAST, GRANT_RECEIVE_BROADCAST, GRANT_MANAGE. |
terminationRules | array of TerminationRule | The 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.
| Field | Type | Description |
|---|---|---|
idleDeadlineMs | string (64-bit integer) | The idle limit: no client attached since this was armed, and at this instant the session ends idleTimeout. |
maxTimeDeadlineMs | string (64-bit integer) | maxSessionTime measured from open. |
codeRotatesAtMs | string (64-bit integer) | The next join-code step boundary. |
holdReleaseAtMs | string (64-bit integer) | A hold taken with a release instant; 0 for a hold that stands until somebody releases it. |
releasedCeilingDeadlineMs | string (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.
| Field | Type | Description |
|---|---|---|
maxClients | number | How many clients a session holds at once; 0 in a patch leaves the default alone. |
reconnectBudget | string | Duration string; the empty string in a patch leaves the default alone. |
backgroundBudget | string | Duration string; the empty string in a patch leaves the default alone. |
hostResumeWindow | string | Host grace, a duration string; 0s ends a session with its host. |
idleTimeout | string | The idle limit, a duration string; 0s is unlimited. |
maxSessionTime | string | A duration string; 0s is no maximum. |
joinCodeRotation | string | A duration string of whole seconds. |
warningLead | string | A 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.
| Field | Type | Description |
|---|---|---|
bytesPerSecond | number | The bytesPerSecond row, per client per direction. |
messagesPerSecond | number | The messagesPerSecond row's bitskiff.in rate, per client: 120 unless the project lowered it. |
linkMessagesPerSecond | number | The 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.
| Field | Type | Description |
|---|---|---|
id | string | The 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. |
sessionId | string | The session. |
projectId | string | The project the session belongs to. |
region | string | The region the session ran in. |
keyId | string | The key that opened the session, or minted the token that did. |
subject | string | The subject the session was opened under: your own id for the customer the host ran for. Empty when no token opened it. |
label | string | The host's label for the session, as it ended. |
metadata | object, string keys to string | The session's own keys and values, as it ended. |
openedAt | string (64-bit integer) | When the session was opened, Unix ms. |
endedAt | string (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. |
endedReason | string | Why 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. |
attachedMs | string (64-bit integer) | Time with a host attached, in milliseconds. |
detachedMs | string (64-bit integer) | Time in host grace or with no host at all, in milliseconds. |
seatMs | string (64-bit integer) | Client time in milliseconds: every client's time connected, away or disconnected, summed over every bundle. Joining is not counted. |
seatMsByBundle | object, string keys to string (64-bit integer) | The same client time, one entry per bundle name, for a price that differs by bundle. |
clients | number | Clients 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. |
peakClients | number | The most clients held at once. |
bytesReported | string (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. |
dataBytesDown | string (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. |
dataBytesUp | string (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. |
dataBytesGrade | string | server_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. |
writtenAt | string (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. |
writerRevision | number | Which revision of the ledger's writer wrote the record. Absent on the webhook, the view and a day record. |
day | string (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. |
complete | boolean | A 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.
| Field | Type | Description |
|---|---|---|
sessionId | string | The session's id. |
projectId | string | The 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). |
config | SessionConfig | Clamped. |
ceilings | SessionConfig | What the project may request. |
label | string | Your own name for the session: what it was opened with. |
metadata | object, string keys to string | Your own keys and values, as the session was opened with. |
region | string | The region the session runs in. |
hostEpoch | number | Counts 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. |
state | string | Where the session is: one of open, host_attached, host_away, host_released or ended. |
openedAt | string (64-bit integer) | When the session was opened, Unix ms. |
endedAt | string (64-bit integer) | 0 while live. |
endedReason | string | Why 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. |
participants | array of ParticipantView | The 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. |
deadlines | SessionDeadlines | 16 is left for the hold member the session hold routes add. |
serverTimeMs | string (64-bit integer) | The service's own Unix ms at the moment it answered. |
e2ee | boolean | Whether 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. |
controllersHeld | number | How 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. |
viewersHeld | number | How many of the clients held at that moment hold no input grant. |
subject | string | The subject the session was opened under: the user id your backend minted the opening token for. Empty when no token opened it. |
keyId | string | The 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. |
usage | SessionUsage | The 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. |
messagesToHost | string (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. |
messagesToClient | string (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.
| Field | Type | Description |
|---|---|---|
name | string | The provider: google or microsoft, as in /v1/auth/sign-in/oidc/{provider}. |
enabled | boolean | True 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.
| Field | Type | Description |
|---|---|---|
windowMs | number | What the RttSummary window figures cover; 60000 nominal, shorter for the report leave sends. |
traffic | array of TrafficCounter | This client's own counts. |
rtt | RttSummary | Round trip over the window, summarised rather than sample by sample. |
path | string | The selected candidate pair now: "udp" | "tcp" | "turn" | "unknown". |
iceRttMs | number | The engine's figure for the selected pair; 0 if none. |
joinToWelcomeMs | number | First 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.
| Field | Type | Description |
|---|---|---|
values | array of string | The 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.
| Field | Type | Description |
|---|---|---|
name | string | This 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. |
when | string | The 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. |
for | string | How 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.
| Field | Type | Description |
|---|---|---|
values | array of TerminationRule | The 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.
| Field | Type | Description |
|---|---|---|
sessionId | string | The session this ticket is for. |
participantId | string | The client this ticket is for, by its id on this wire. |
grants | array of string | What 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. |
protocol | number | The protocol version the service pinned for this session, which every end of it speaks. |
endpoint | string | Where 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. |
token | string | A signed token for the media engine, handed to the SDK unchanged. |
expiresAt | string (64-bit integer) | Unix ms; 60 s after issue. |
region | string | The region the transport is dialled into. |
jti | string | Single-use id; consumed by the service on issue and reconciled on join. |
opener | boolean | True for the host. |
apiId | string | The sender identity the service itself signs with in this region, a_<region>: the only sender that is not another client and is still trusted. |
kind | string | Which 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.
| Field | Type | Description |
|---|---|---|
jkt | string | The SHA-256 JWK thumbprint of the client's key. |
TokenLimits
The limits a mint binds into a token.
| Field | Type | Description |
|---|---|---|
concurrentSessions | number | Defaults to 1 when the mint omits it. |
clientMinutesPerDay | number | How many client minutes a day this token's subject may spend. |
concurrentClients | number | Per subject; a join token's default is 1. |
sessionOpensPerMinute | number | Per subject; default 3. |
joinsPerMinute | number | Per subject; default 6. |
TokenSummary
One row of GET /v1/tokens.
| Field | Type | Description |
|---|---|---|
tokenId | string | The token's id, which is what DELETE /v1/tokens/{tokenId} names. |
subject | string | Who the token was minted for: your own id for a person or a device. |
keyId | string | The minting key. |
permissions | array of string | What the token may do, as a narrowing of the key that minted it. |
mintedAt | string (64-bit integer) | When the token was minted, Unix ms. |
expiresAt | string (64-bit integer) | The token's own expiry, before the project's current ceiling. |
sessionId | string | A join token's binding, or empty. |
grantsCeiling | array of string | A 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.
| Field | Type | Description |
|---|---|---|
topic | string | "ctl" | "in" | "out". |
direction | string | "to_host" | "to_client", named from the client's side. |
path | string | "udp" | "tcp" | "turn" | "unknown" while these were counted on the engine, and "direct" for the direct link. |
messagesCount | string (64-bit integer) | How many messages were counted. |
bytes | string (64-bit integer) | How many bytes were counted. |
droppedCount | string (64-bit integer) | SDK-side drops. |
transport | string | The 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.
| Field | Type | Description |
|---|---|---|
messages | string (64-bit integer) | How many messages. |
bytes | string (64-bit integer) | How many bytes, as the host reported them. |
TransportTraffic
One transport's host-reported traffic on a session, summed over topics.
| Field | Type | Description |
|---|---|---|
name | string | The transport's name: server, peer, or the name the host declared, such as bluetooth. |
in | TrafficTotals | What went to the host on this transport. |
out | TrafficTotals | What went to clients on this transport. |
UsageRow
One bucket of usage: when it starts, and what each meter counted in it.
| Field | Type | Description |
|---|---|---|
bucketStart | string (64-bit integer) | Start of the UTC day or month. |
meters | object, string keys to number | Meter name -> value, in the metering vocabulary. |