openapi: 3.0.3
info:
title: Obscura Server API
description: |
A privacy-first messaging relay.
- **Control Plane (REST):** Registration, 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
paths:
/v1/users:
post:
summary: Register a new user device.
tags: [Account]
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'
'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'
/v1/sessions:
post:
summary: Login and retrieve a session token.
tags: [Account]
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'
'429':
$ref: '#/components/responses/TooManyRequestsError'
delete:
summary: Logout (Revoke Refresh Token).
description: |
Revokes the provided Refresh Token, effectively logging the user out of that session.
tags: [Account]
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'
/v1/sessions/refresh:
post:
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: [Account]
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'
/v1/keys:
post:
summary: Upload PreKeys or Perform Device Takeover.
description: |
Uploads new Signed PreKeys and One-Time PreKeys.
**Device Takeover:**
If an `identityKey` is provided and it *differs* from the stored key for this user:
- Replaces the Identity Key.
- Deletes ALL old keys (Signed and One-Time).
- Deletes ALL pending messages.
- Disconnects active WebSockets.
If `identityKey` matches the stored key or is omitted, it acts as a standard key refill (appending new keys).
tags: [Keys]
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'
'429':
$ref: '#/components/responses/TooManyRequestsError'
/v1/keys/{userId}:
get:
summary: Fetch PreKey Bundle.
description: |
Returns active PreKey bundle for the target user.
Server atomically consumes one One-Time PreKey (if available).
tags: [Keys]
parameters:
- name: userId
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: PreKey Bundle.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/json:
schema:
$ref: '#/components/schemas/PreKeyBundleResponse'
'404':
$ref: '#/components/responses/NotFoundError'
'429':
$ref: '#/components/responses/TooManyRequestsError'
/v1/messages/{recipientId}:
post:
summary: Send an encrypted message (Push).
description: |
Pushes an encrypted envelope to the target device's queue.
**Payload:** `EncryptedMessage` (Protobuf).
The recipient will receive this via WebSocket.
tags: [Messaging]
security:
- bearerAuth: []
parameters:
- name: recipientId
in: path
required: true
schema:
type: string
format: uuid
requestBody:
content:
application/x-protobuf:
schema:
type: string
format: binary
description: Serialized `EncryptedMessage` protobuf.
application/octet-stream:
schema:
type: string
format: binary
description: Serialized `EncryptedMessage` protobuf.
responses:
'201':
description: Message queued.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
'400':
$ref: '#/components/responses/BadRequestError'
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
$ref: '#/components/responses/NotFoundError'
'429':
$ref: '#/components/responses/TooManyRequestsError'
/v1/gateway:
get:
summary: Connect to the Message Stream.
description: |
**WebSocket Endpoint.**
Clients must connect here to receive messages.
- **Protocol:** `WebSocketFrame` (Protobuf).
- **Auth:** Pass the JWT in the query string: `ws://.../v1/gateway?token=<jwt>`.
- **Handshake:** Server validates the JWT and ensures the user has an Identity Key stored.
- **Welcome:** Upon successful connection, the server may immediately push a `PreKeyStatus` frame if the user'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]
parameters:
- name: token
in: query
required: true
schema:
type: string
description: JWT Access Token.
responses:
'101':
description: Switching Protocols.
'401':
$ref: '#/components/responses/UnauthorizedError'
'429':
$ref: '#/components/responses/TooManyRequestsError'
/v1/attachments:
post:
summary: Upload an attachment.
description: |
Uploads an encrypted binary blob to long-term storage (S3).
Blob will be auto-deleted after the configured TTL.
Max size is enforced via the `storage-max-size-bytes` configuration.
tags: [Attachments]
security:
- bearerAuth: []
requestBody:
content:
application/octet-stream:
schema:
type: string
format: binary
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'
'429':
$ref: '#/components/responses/TooManyRequestsError'
/v1/attachments/{id}:
get:
summary: Download an attachment.
tags: [Attachments]
security:
- bearerAuth: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Binary file stream.
headers:
x-request-id:
$ref: '#/components/headers/x-request-id'
content:
application/octet-stream:
schema:
type: string
format: binary
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
$ref: '#/components/responses/NotFoundError'
'429':
$ref: '#/components/responses/TooManyRequestsError'
/v1/push-tokens:
put:
summary: Register or update a push notification token.
description: |
Associates an FCM or APNS token with the authenticated user device for wake-up signals.
Obscura uses a "Single Device" policy; updating the token replaces any existing one.
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'
'401':
$ref: '#/components/responses/UnauthorizedError'
'400':
$ref: '#/components/responses/BadRequestError'
'429':
$ref: '#/components/responses/TooManyRequestsError'
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
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 or malformed request.
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'
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'
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.
LoginRequest:
type: object
required:
- username
- password
properties:
username:
type: string
password:
type: string
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
- identityKey
- registrationId
- signedPreKey
- oneTimePreKeys
properties:
username:
type: string
password:
type: string
identityKey:
type: string
format: byte
description: Base64 encoded public identity key.
registrationId:
type: integer
signedPreKey:
$ref: '#/components/schemas/SignedPreKey'
oneTimePreKeys:
type: array
items:
$ref: '#/components/schemas/OneTimePreKey'
PreKeyUploadRequest:
type: object
required:
- signedPreKey
- oneTimePreKeys
properties:
identityKey:
type: string
format: byte
description: Optional Base64 Identity Key. Triggers takeover if changed.
registrationId:
type: integer
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:
registrationId:
type: integer
identityKey:
type: string
format: byte
signedPreKey:
$ref: '#/components/schemas/SignedPreKey'
oneTimePreKey:
nullable: true
allOf:
- $ref: '#/components/schemas/OneTimePreKey'
SignedPreKey:
type: object
required:
- keyId
- publicKey
- signature
properties:
keyId:
type: integer
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
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.