This document describes the HTTP and WebSocket API so new clients can be implemented.
The server listens at DIALTONE_LISTEN_ADDR. Use HTTPS in production.
Usernames are sent in plaintext during login/register so the server can authenticate. The server stores only a peppered hash (no plaintext usernames in the database).
All authenticated HTTP requests use a bearer token:
Authorization: Bearer <token>
WebSocket connections use the same header during the handshake.
HTTP errors return JSON:
{"error":"message"}Common status codes:
- 400: invalid input
- 401: unauthorized
- 403: forbidden
- 404: not found
- 410: invite expired or consumed
- 500: server error
GET /health
Returns 200 OK with ok.
POST /auth/register
Request:
{
"username": "string",
"password": "string",
"public_key": "base64",
"invite_token": "string"
}Response:
{
"token": "string",
"user_id": "string",
"device_id": "string",
"expires_at": "RFC3339",
"username": "string",
"device_public_key": "base64",
"is_admin": true,
"is_trusted": true
}POST /auth/login
Request:
{
"username": "string",
"password": "string",
"public_key": "base64"
}Response is the same as register.
POST /users
Request:
{ "username": "string" }Response:
{ "id": "string", "username": "string", "created_at": "RFC3339" }GET /users/profiles
Response:
{ "profiles": [ { "user_id": "string", "name_enc": "string", "updated_at": "RFC3339" } ] }POST /users/profiles
Request:
{ "name_enc": "string" }Response:
{ "status": "ok" }POST /devices
Request:
{ "user_id": "string", "public_key": "base64" }Response:
{ "id": "string", "user_id": "string", "public_key": "base64", "created_at": "RFC3339" }GET /devices/keys?user_id=<id>
Response:
{ "user_id": "string", "keys": [ { "device_id": "string", "public_key": "base64" } ] }GET /devices/keys?all=1
Response:
{ "keys": [ { "user_id": "string", "device_id": "string", "public_key": "base64" } ] }GET /channels
Response:
{ "channels": [ { "id": "string", "name_enc": "string", "created_by": "string", "created_at": "RFC3339" } ] }POST /channels
Admin only.
Request:
{ "name_enc": "string" }Response:
{ "channel": { "id": "string", "name_enc": "string", "created_by": "string", "created_at": "RFC3339" } }PATCH /channels
Admin only.
Request:
{ "channel_id": "string", "name_enc": "string" }Response matches create.
DELETE /channels?channel_id=<id>
Admin only. Returns 204 No Content on success.
GET /channels/messages?channel_id=<id>&limit=<n>
Response:
{
"channel_id": "string",
"messages": [
{
"id": "string",
"channel_id": "string",
"sender_id": "string",
"sender_name_enc": "string",
"body": "string",
"sent_at": "RFC3339"
}
]
}GET /channels/keys?channel_id=<id>
Response:
{
"channel_id": "string",
"device_id": "string",
"sender_device_id": "string",
"sender_public_key": "base64",
"envelope": "string",
"created_at": "RFC3339"
}POST /channels/keys
Request:
{
"channel_id": "string",
"envelopes": [
{
"device_id": "string",
"sender_device_id": "string",
"sender_public_key": "base64",
"envelope": "string"
}
]
}Response:
{ "status": "ok" }GET /directory/keys
200 OKwith envelope204 No Contentif no envelope exists
Response:
{
"device_id": "string",
"sender_device_id": "string",
"sender_public_key": "base64",
"envelope": "string",
"created_at": "RFC3339"
}POST /directory/keys
Request:
{
"envelopes": [
{
"device_id": "string",
"sender_device_id": "string",
"sender_public_key": "base64",
"envelope": "string"
}
]
}Response:
{ "status": "ok" }POST /presence
Request:
{ "user_ids": ["string"] }Response:
{ "statuses": { "user_id": true }, "admins": { "user_id": false } }POST /server/invites
Admin token header is required:
X-Admin-Token: <token>
Response:
{ "token": "string", "expires_at": "RFC3339" }GET /ws
Requires Authorization: Bearer <token> header.
channel.message.send
{
"type": "channel.message.send",
"channel_id": "string",
"body": "encrypted message",
"sender_name_enc": "encrypted sender name"
}voice_join
{
"type": "voice_join",
"channel_id": "string"
}voice_leave
{
"type": "voice_leave",
"channel_id": "string"
}webrtc_offer
{
"type": "webrtc_offer",
"channel_id": "string",
"recipient": "string",
"sdp": "string"
}webrtc_answer
{
"type": "webrtc_answer",
"channel_id": "string",
"recipient": "string",
"sdp": "string"
}ice_candidate
{
"type": "ice_candidate",
"channel_id": "string",
"recipient": "string",
"candidate": "string"
}
### Server -> Client events
`channel.message.new`
```json
{
"type": "channel.message.new",
"message_id": "string",
"channel_id": "string",
"sender": "string",
"sender_name_enc": "string",
"body": "string",
"sent_at": "RFC3339"
}
channel.updated
{
"type": "channel.updated",
"channel_id": "string",
"channel_name_enc": "string"
}channel.deleted
{
"type": "channel.deleted",
"channel_id": "string"
}voice_join
{
"type": "voice_join",
"channel_id": "string",
"sender": "string"
}voice_leave
{
"type": "voice_leave",
"channel_id": "string",
"sender": "string"
}webrtc_offer
{
"type": "webrtc_offer",
"channel_id": "string",
"sender": "string",
"recipient": "string",
"sdp": "string"
}webrtc_answer
{
"type": "webrtc_answer",
"channel_id": "string",
"sender": "string",
"recipient": "string",
"sdp": "string"
}ice_candidate
{
"type": "ice_candidate",
"channel_id": "string",
"sender": "string",
"recipient": "string",
"candidate": "string"
}device.joined
{
"type": "device.joined",
"sender": "string",
"device_id": "string"
}user.profile.updated
{
"type": "user.profile.updated",
"sender": "string"
}error
{
"type": "error",
"code": "string",
"message": "string"
}