openapi: 3.0.3
info:
title: Obscura Server API
description: |
A privacy-first messaging relay.
- **Control Plane (REST):** Registration, Device Management, Key Management, Sending.
- **Data Plane (WebSocket):** Real-time message delivery and acknowledgement.
- **Protocol:** Protocol Buffers (application/x-protobuf) are used for message bodies to minimize metadata leakage and binary bloat.
**Protobuf Definitions:**
The WebSocket and Message schemas are defined in the [obscura-proto](https://github.com/barrelmaker97/obscura-proto/blob/main/obscura/v1/obscura.proto) repository.
version: 0.0.0
license:
name: GPL-3.0-or-later
url: https://www.gnu.org/licenses/gpl-3.0.html
security:
- bearerAuth: []
paths:
/v1/users:
post:
operationId: registerUser
summary: Register a new user account.
description: |
Creates a new user account with the provided credentials.
Returns a User-Scoped JWT (no `deviceId` claim). The client must then
create a device via `POST /v1/devices` to obtain a Device-Scoped JWT
required for key management, messaging, and WebSocket connections.
tags: [Users]
security: []
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/RegistrationRequest'
responses:
'201':
description: Account created.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/AuthResponse'
'400':
$ref: '#/components/responses/BadRequestError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'409':
description: Username already exists.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'429':
$ref: '#/components/responses/TooManyRequestsError'
'500':
$ref: '#/components/responses/InternalServerError'
/v1/users/{userId}:
get:
operationId: getPreKeyBundle
summary: Fetch PreKey Bundles for all of a user's devices.
description: |
Returns an array of PreKey bundles, one per device registered to the target user.
Server atomically consumes one One-Time PreKey per device (if available).
Requires a Device-Scoped JWT (the caller's own device must be identified).
tags: [Users]
parameters:
- name: userId
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Array of PreKey Bundles, one per device.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/PreKeyBundleResponse'
'401':
$ref: '#/components/responses/UnauthorizedError'
'403':
$ref: '#/components/responses/ForbiddenError'
'404':
$ref: '#/components/responses/NotFoundError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'429':
$ref: '#/components/responses/TooManyRequestsError'
'500':
$ref: '#/components/responses/InternalServerError'
/v1/sessions:
post:
operationId: login
summary: Login and retrieve a session token.
description: |
Authenticates with username and password. If an optional `deviceId` is provided
and belongs to the user, the returned JWT will include that `deviceId` claim
(Device-Scoped). Otherwise, a User-Scoped JWT is returned.
tags: [Sessions]
security: []
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
responses:
'200':
description: Authenticated.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/AuthResponse'
'401':
$ref: '#/components/responses/UnauthorizedError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'429':
$ref: '#/components/responses/TooManyRequestsError'
'500':
$ref: '#/components/responses/InternalServerError'
delete:
operationId: logout
summary: Logout (Revoke Refresh Token).
description: |
Revokes the provided Refresh Token, effectively logging the user out of that session.
tags: [Sessions]
security:
- bearerAuth: []
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/LogoutRequest'
responses:
'200':
description: Session revoked.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
'401':
$ref: '#/components/responses/UnauthorizedError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'500':
$ref: '#/components/responses/InternalServerError'
/v1/sessions/refresh:
post:
operationId: refreshToken
summary: Refresh Access Token.
description: |
Exchanges a valid Refresh Token for a new pair (Access Token + Refresh Token).
Implements Rotation: The old Refresh Token is invalidated immediately.
tags: [Sessions]
security: []
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/RefreshRequest'
responses:
'200':
description: Tokens refreshed.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/AuthResponse'
'401':
$ref: '#/components/responses/UnauthorizedError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'500':
$ref: '#/components/responses/InternalServerError'
/v1/devices:
post:
operationId: createDevice
summary: Provision a new device.
description: |
Creates a new device for the authenticated user, uploads its cryptographic keys,
and returns a Device-Scoped JWT. Requires a User-Scoped or Device-Scoped JWT.
The server enforces a configurable maximum number of devices per user.
tags: [Devices]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateDeviceRequest'
responses:
'201':
description: Device provisioned.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/AuthResponse'
'400':
$ref: '#/components/responses/BadRequestError'
'401':
$ref: '#/components/responses/UnauthorizedError'
'403':
$ref: '#/components/responses/ForbiddenError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'429':
$ref: '#/components/responses/TooManyRequestsError'
'500':
$ref: '#/components/responses/InternalServerError'
get:
operationId: listDevices
summary: List all devices for the authenticated user.
tags: [Devices]
security:
- bearerAuth: []
responses:
'200':
description: List of devices.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/DeviceListResponse'
'401':
$ref: '#/components/responses/UnauthorizedError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'500':
$ref: '#/components/responses/InternalServerError'
/v1/devices/{deviceId}:
get:
operationId: getDevice
summary: Get a single device.
description: |
Returns the details of a specific device owned by the authenticated user.
tags: [Devices]
security:
- bearerAuth: []
parameters:
- name: deviceId
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Device details.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/DeviceResponse'
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
$ref: '#/components/responses/NotFoundError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'500':
$ref: '#/components/responses/InternalServerError'
put:
operationId: updateDevice
summary: Update a device.
description: |
Updates the metadata (e.g., name) of a device owned by the authenticated user.
tags: [Devices]
security:
- bearerAuth: []
parameters:
- name: deviceId
in: path
required: true
schema:
type: string
format: uuid
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateDeviceRequest'
responses:
'200':
description: Device updated.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/DeviceResponse'
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
$ref: '#/components/responses/NotFoundError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'500':
$ref: '#/components/responses/InternalServerError'
delete:
operationId: deleteDevice
summary: Delete a device.
description: |
Deletes a device owned by the authenticated user. Cascade delete removes
all associated keys, pending messages, and push tokens.
tags: [Devices]
security:
- bearerAuth: []
parameters:
- name: deviceId
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Device deleted.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
$ref: '#/components/responses/NotFoundError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'500':
$ref: '#/components/responses/InternalServerError'
/v1/devices/keys:
post:
operationId: uploadKeys
summary: Upload PreKeys or Perform Device Takeover.
description: |
Uploads new Signed PreKeys and One-Time PreKeys for the authenticated device.
Requires a Device-Scoped JWT.
**Device Takeover:**
If an `identityKey` is provided and it *differs* from the stored key for this device:
- Replaces the Identity Key.
- Deletes ALL old keys (Signed and One-Time).
- Deletes ALL pending messages for this device.
- Disconnects active WebSockets for this device.
If `identityKey` matches the stored key or is omitted, it acts as a standard key refill (appending new keys).
tags: [Devices]
security:
- bearerAuth: []
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PreKeyUploadRequest'
responses:
'200':
description: Keys updated or takeover successful.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
'400':
$ref: '#/components/responses/BadRequestError'
'401':
$ref: '#/components/responses/UnauthorizedError'
'403':
$ref: '#/components/responses/ForbiddenError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'429':
$ref: '#/components/responses/TooManyRequestsError'
'500':
$ref: '#/components/responses/InternalServerError'
/v1/messages:
post:
operationId: sendMessages
summary: Send one or more encrypted messages.
description: |
Pushes an array of encrypted envelopes to target devices' queues.
Accepts a batch of messages to support multi-device fan-out or single messages.
**Idempotency:** Requires an `Idempotency-Key` header to safely retry dropped network requests.
**Payload:** `SendMessageRequest` (Protobuf).
**Response:** `SendMessageResponse` (Protobuf) detailing any partial failures. An empty response array indicates total success.
tags: [Messaging]
security:
- bearerAuth: []
parameters:
- name: Idempotency-Key
in: header
required: true
description: Client-generated UUID. Used to prevent duplicate processing on network retries.
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/x-protobuf:
schema:
type: string
format: binary
description: Serialized `SendMessageRequest` protobuf.
responses:
'200':
description: Batch processed. Parse the response payload to check for partial failures via the `failed_messages` array.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/x-protobuf:
schema:
type: string
format: binary
description: Serialized `SendMessageResponse` protobuf.
'400':
$ref: '#/components/responses/BadRequestError'
'401':
$ref: '#/components/responses/UnauthorizedError'
'403':
$ref: '#/components/responses/ForbiddenError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'413':
description: Payload Too Large. The batch contains too many messages (exceeds server max).
'429':
$ref: '#/components/responses/TooManyRequestsError'
'500':
$ref: '#/components/responses/InternalServerError'
/v1/gateway/ticket:
post:
operationId: generateTicket
summary: Generate a WebSocket connection ticket.
description: |
Generates a short-lived, single-use ticket for establishing a WebSocket connection.
This allows the client to authenticate via the `Authorization` header once, and then
pass the ticket as a query parameter in the WebSocket handshake (where custom headers are often difficult to set).
Requires a Device-Scoped JWT.
tags: [Messaging]
security:
- bearerAuth: []
responses:
'201':
description: Ticket generated.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/TicketResponse'
'401':
$ref: '#/components/responses/UnauthorizedError'
'403':
$ref: '#/components/responses/ForbiddenError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'429':
$ref: '#/components/responses/TooManyRequestsError'
'500':
$ref: '#/components/responses/InternalServerError'
/v1/gateway:
get:
operationId: connectGateway
summary: Connect to the Message Stream.
description: |
**WebSocket Endpoint.**
Clients must connect here to receive messages for a specific device.
- **Protocol:** `WebSocketFrame` (Protobuf).
- **Auth:** Pass a valid ticket in the query string: `ws://.../v1/gateway?ticket=<ticket>`.
- **Handshake:** Server validates the ticket, ensuring it exists and hasn't expired or been used.
- **Welcome:** Upon successful connection, the server may immediately push a `PreKeyStatus` frame if the device's one-time pre-key count is below the configured threshold.
- **Flow:** Server pushes `Envelope` frames. Client MUST respond with `AckMessage` frames. Server batches deletions based on ACKs.
tags: [Messaging]
security:
- ticketAuth: []
parameters:
- name: ticket
in: query
required: true
schema:
type: string
description: Single-use WebSocket authentication ticket.
responses:
'101':
description: Switching Protocols.
'401':
$ref: '#/components/responses/UnauthorizedError'
'429':
$ref: '#/components/responses/TooManyRequestsError'
'500':
$ref: '#/components/responses/InternalServerError'
/v1/attachments:
post:
operationId: uploadAttachment
summary: Upload an attachment.
description: |
Uploads an encrypted binary blob to long-term storage.
Blobs are automatically deleted after a configured period.
Maximum and minimum size limits are enforced by the server.
tags: [Attachments]
security:
- bearerAuth: []
requestBody:
content:
application/octet-stream:
schema:
type: string
format: binary
minLength: 1
responses:
'201':
description: Upload successful.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/AttachmentResponse'
'400':
$ref: '#/components/responses/BadRequestError'
'401':
$ref: '#/components/responses/UnauthorizedError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'411':
$ref: '#/components/responses/LengthRequiredError'
'413':
$ref: '#/components/responses/PayloadTooLargeError'
'429':
$ref: '#/components/responses/TooManyRequestsError'
'500':
$ref: '#/components/responses/InternalServerError'
/v1/attachments/{id}:
get:
operationId: downloadAttachment
summary: Download an attachment.
tags: [Attachments]
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
- name: If-None-Match
in: header
required: false
schema:
type: string
description: The unique identifier for this attachment (from ETag).
responses:
'200':
description: Binary file stream.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
ETag:
description: The unique identifier for this attachment version.
schema:
type: string
content:
application/octet-stream:
schema:
type: string
format: binary
'304':
description: Attachment not modified (client already has the latest version).
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
$ref: '#/components/responses/NotFoundError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'429':
$ref: '#/components/responses/TooManyRequestsError'
'500':
$ref: '#/components/responses/InternalServerError'
/v1/backup:
get:
operationId: getBackup
summary: Download the latest backup.
description: |
Streams the latest encrypted backup blob.
Supports conditional caching via `If-None-Match`.
Returns `ETag` header containing the version number.
tags: [Backup]
security:
- bearerAuth: []
parameters:
- name: If-None-Match
in: header
required: false
schema:
type: string
description: The current version held by the client (e.g. from ETag).
responses:
'200':
description: Binary backup stream.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
ETag:
description: Current backup version.
schema:
type: string
content:
application/octet-stream:
schema:
type: string
format: binary
'304':
description: Backup not modified (client already has the latest version).
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
'401':
$ref: '#/components/responses/UnauthorizedError'
'403':
$ref: '#/components/responses/ForbiddenError'
'404':
$ref: '#/components/responses/NotFoundError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'429':
$ref: '#/components/responses/TooManyRequestsError'
'500':
$ref: '#/components/responses/InternalServerError'
head:
operationId: headBackup
summary: Check for backup existence.
description: |
Returns metadata (Content-Length, ETag) without the body.
Useful for checking if a local backup is out of date.
tags: [Backup]
security:
- bearerAuth: []
responses:
'200':
description: Backup exists.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
ETag:
description: Current backup version.
schema:
type: string
Content-Length:
schema:
type: integer
format: int32
'401':
$ref: '#/components/responses/UnauthorizedError'
'403':
$ref: '#/components/responses/ForbiddenError'
'404':
$ref: '#/components/responses/NotFoundError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'429':
$ref: '#/components/responses/TooManyRequestsError'
'500':
$ref: '#/components/responses/InternalServerError'
post:
operationId: uploadBackup
summary: Upload a new backup.
description: |
Uploads a new encrypted backup blob.
Uses Optimistic Locking via headers to prevent concurrent overwrites.
**Flow:**
1. **Initial Upload:** Client MUST send `If-None-Match: *`.
2. **Update:** Client MUST send `If-Match` with the latest version number (as received in a previous `ETag`).
3. If the preconditions match and no other upload is in progress, the server accepts the stream.
4. If the version does not match or resource already exists when `*` is used, server returns `412 Precondition Failed`.
5. If another upload is already in progress, server returns `409 Conflict`.
tags: [Backup]
security:
- bearerAuth: []
parameters:
- name: If-Match
in: header
required: false
schema:
type: string
description: Current version held by the client (from ETag). Required for updates.
- name: If-None-Match
in: header
required: false
schema:
type: string
description: Set to "*" for the first upload to ensure no backup exists. Required for initial upload.
requestBody:
content:
application/octet-stream:
schema:
type: string
format: binary
minLength: 32
maxLength: 2097152
responses:
'200':
description: Upload successful.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
ETag:
description: The new backup version.
schema:
type: string
'400':
$ref: '#/components/responses/BadRequestError'
'401':
$ref: '#/components/responses/UnauthorizedError'
'403':
$ref: '#/components/responses/ForbiddenError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'409':
$ref: '#/components/responses/ConflictError'
'411':
$ref: '#/components/responses/LengthRequiredError'
'412':
$ref: '#/components/responses/PreconditionFailedError'
'413':
$ref: '#/components/responses/PayloadTooLargeError'
'429':
$ref: '#/components/responses/TooManyRequestsError'
'500':
$ref: '#/components/responses/InternalServerError'
/v1/push-tokens:
put:
operationId: updatePushToken
summary: Register or update a push notification token.
description: |
Associates an FCM or APNS token with the authenticated device for wake-up signals.
Each device has its own push token. Updating the token replaces any existing one for this device.
Requires a Device-Scoped JWT.
tags: [Push Notifications]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterPushTokenRequest'
responses:
'200':
description: Token registered successfully.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
'400':
$ref: '#/components/responses/BadRequestError'
'401':
$ref: '#/components/responses/UnauthorizedError'
'403':
$ref: '#/components/responses/ForbiddenError'
'408':
$ref: '#/components/responses/RequestTimeoutError'
'429':
$ref: '#/components/responses/TooManyRequestsError'
'500':
$ref: '#/components/responses/InternalServerError'
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
ticketAuth:
type: apiKey
in: query
name: ticket
headers:
x-request-id:
description: Unique identifier for the request, used for tracing and debugging.
schema:
type: string
format: uuid
retry-after:
description: The number of seconds to wait before retrying the request.
schema:
type: string
responses:
UnauthorizedError:
description: Unauthorized (Invalid or missing token).
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
BadRequestError:
description: Invalid input, malformed request, or missing required headers (e.g. version or length).
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
ForbiddenError:
description: Forbidden (Action not allowed or limit reached).
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
NotFoundError:
description: The requested resource was not found.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
ConflictError:
description: Conflict (e.g., concurrent upload already in progress).
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
PreconditionFailedError:
description: Precondition Failed (Version mismatch or resource already exists).
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
RequestTimeoutError:
description: Request Timeout (The server timed out waiting for the request).
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
LengthRequiredError:
description: Length Required (Content-Length header missing).
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
PayloadTooLargeError:
description: Payload Too Large (Upload exceeds maximum size limit).
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
TooManyRequestsError:
description: Rate limit exceeded.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
retry-after:
$ref: '#/components/headers/retry-after'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
InternalServerError:
description: Internal Server Error.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
schemas:
ErrorResponse:
type: object
required: [error]
properties:
error:
type: string
description: Semantic error message.
AuthResponse:
type: object
required:
- token
- refreshToken
- expiresAt
properties:
token:
type: string
description: Short-lived JWT Access Token.
refreshToken:
type: string
description: Long-lived opaque Refresh Token.
expiresAt:
type: integer
format: int64
description: UNIX timestamp of access token expiration.
deviceId:
type: string
format: uuid
description: Present when a Device-Scoped JWT is returned (e.g. after device creation or login with deviceId).
LoginRequest:
type: object
required:
- username
- password
properties:
username:
type: string
minLength: 1
maxLength: 50
password:
type: string
minLength: 12
deviceId:
type: string
format: uuid
description: Optional. If provided and belongs to the user, the returned JWT will be Device-Scoped.
LogoutRequest:
type: object
required: [refreshToken]
properties:
refreshToken:
type: string
RefreshRequest:
type: object
required: [refreshToken]
properties:
refreshToken:
type: string
RegisterPushTokenRequest:
type: object
required: [token]
properties:
token:
type: string
description: FCM or APNS device token.
RegistrationRequest:
type: object
required:
- username
- password
properties:
username:
type: string
minLength: 1
maxLength: 50
password:
type: string
minLength: 12
CreateDeviceRequest:
type: object
required:
- identityKey
- registrationId
- signedPreKey
- oneTimePreKeys
properties:
name:
type: string
maxLength: 100
description: Optional human-readable label for the device (e.g. "My Phone").
identityKey:
type: string
format: byte
description: Base64 encoded public identity key.
registrationId:
type: integer
format: int32
signedPreKey:
$ref: '#/components/schemas/SignedPreKey'
oneTimePreKeys:
type: array
items:
$ref: '#/components/schemas/OneTimePreKey'
DeviceResponse:
type: object
properties:
deviceId:
type: string
format: uuid
name:
type: string
nullable: true
createdAt:
type: string
format: date-time
DeviceListResponse:
type: object
required: [devices]
properties:
devices:
type: array
items:
$ref: '#/components/schemas/DeviceResponse'
UpdateDeviceRequest:
type: object
properties:
name:
type: string
nullable: true
description: Optional human-readable label for the device.
PreKeyUploadRequest:
type: object
required:
- signedPreKey
- oneTimePreKeys
properties:
identityKey:
type: string
format: byte
description: Optional Base64 Identity Key. Triggers takeover if changed.
registrationId:
type: integer
format: int32
description: Required if identityKey is provided.
signedPreKey:
description: Signed Pre-Key. ID must be strictly greater than the current stored ID.
allOf:
- $ref: '#/components/schemas/SignedPreKey'
oneTimePreKeys:
type: array
items:
$ref: '#/components/schemas/OneTimePreKey'
PreKeyBundleResponse:
type: object
properties:
deviceId:
type: string
format: uuid
description: The device this bundle belongs to.
registrationId:
type: integer
format: int32
identityKey:
type: string
format: byte
signedPreKey:
$ref: '#/components/schemas/SignedPreKey'
oneTimePreKey:
type: object
nullable: true
allOf:
- $ref: '#/components/schemas/OneTimePreKey'
SignedPreKey:
type: object
required:
- keyId
- publicKey
- signature
properties:
keyId:
type: integer
format: int32
publicKey:
type: string
format: byte
description: Base64 encoded public key
signature:
type: string
format: byte
description: Base64 encoded signature
OneTimePreKey:
type: object
required:
- keyId
- publicKey
properties:
keyId:
type: integer
format: int32
publicKey:
type: string
format: byte
description: Base64 encoded public key
AttachmentResponse:
type: object
properties:
id:
type: string
format: uuid
expiresAt:
type: integer
format: int64
description: UNIX timestamp of when the file will be deleted.
TicketResponse:
type: object
required: [ticket]
properties:
ticket:
type: string
description: A short-lived, single-use authentication ticket.