Join HTTP endpoint
POST /join lets a device obtain a participant token directly, for deployments that do not
(yet) have a backend of their own. A real application usually mints tokens from its own server
with MintClientToken instead — that is the only way to
tie the token to a signed-in user.
| Method / path | POST /join |
| Host | Your deployment’s join host, e.g. https://join.example.moqom.cloud |
| Auth | Authorization: Bearer <device credential> |
| Body | JSON, at most 4 KiB |
| Rate limit | 30 requests per minute per credential |
| Token lifetime | 15 minutes |
What it can and cannot mint
Section titled “What it can and cannot mint”The endpoint deliberately narrows what a device can get:
- No moderation capabilities and no ownership, whatever the credential could otherwise do.
- Publish rights are the credential’s own, never anything named in the request.
- A credential with no publish rights is refused (403) rather than issued a token that can publish nothing. A tenant’s owner-level backend key is in this category — do not ship it to devices.
- Relay keys are refused.
Request
Section titled “Request”{ "tenant": "t_3f9a0c1d2e4b5a69", "username": "alice", "room": "demo", "key": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" }}| Field | Required | Meaning |
|---|---|---|
tenant |
no | Defaults to the credential’s tenant |
username |
yes | The name to join as (normalised server-side) |
room |
no | The room to join |
key |
one of key / device |
The device’s public key as a JWK. The SDK sends this for you (TokenService). |
device |
one of key / device |
Legacy: a device thumbprint. If both are sent they must agree, or the request is refused. |
Depending on deployment configuration, a room that does not exist yet is either created
(LIVE, default policy) or refused with 404.
curl -sS https://join.example.moqom.cloud/join \ -H "Authorization: Bearer $DEVICE_CREDENTIAL" \ -H 'Content-Type: application/json' \ -d '{"username":"alice","room":"demo","key":{"kty":"EC","crv":"P-256","x":"…","y":"…"}}'Response
Section titled “Response”200 OK (Cache-Control: no-store)
{ "token": "eyJhbGciOiJFZERTQSIs…", "expiresAt": "2026-10-06T09:35:00Z", "room": "demo", "username": "alice", "keyId": "k-2026-10", "device": "Xq3v…full-thumbprint…"}| Field | Meaning |
|---|---|
token |
Pass to the client SDK as the session credential |
expiresAt |
When it stops being accepted — schedule renewal from this, never by decoding the token |
username |
The name actually minted, after normalisation. Display this one. |
keyId |
Signing key ID |
device |
The device thumbprint the token was bound to. Compare with the device’s own. |
Note the camelCase field names: this endpoint is consumed directly by devices.
Errors
Section titled “Errors”Error bodies are plain text explaining the cause.
| Status | Cause | Client should |
|---|---|---|
400 |
Unreadable body; missing key/device; mismatched key and device; invalid username |
Fix the request |
401 |
Missing or unknown credential, or a relay credential | Re-provision the credential |
402 |
Tenant has no credit (never paid, or balance spent) | Tell the user the service is unavailable; the tenant must top up |
403 |
Credential has no publish rights, or lacks permission | Stop — the credential will not do |
404 |
Room not found | Show “room not found” |
405 |
Not a POST |
— |
409 |
Room has ended, or another precondition failed | Do not retry blindly |
429 |
Rate limited | Retry shortly |
503 / 504 |
Temporarily unavailable / timed out | Retry with backoff |
500 |
Internal error | Retry later; report if persistent |