Obsidian Email SecurityDocumentation Sign in

API tokens & REST API

The REST API lets PSA / helpdesk tools and scripts search messages, release them, manage lists and read statistics.

Tokens (Manage › API tokens)

Name
What the token is for (e.g. "MyHelpDesk integration"). Appears in the audit log as api:<name>.
Limit to organization
An organization token only sees and changes that organization; "All organizations" tokens can do everything the API offers.

After Create the token is shown once - copy it then; only a hash is stored. The list shows each token's prefix, scope and last use. Revoke disables a token immediately.

Calling the API

Send the token as a bearer header. All responses are JSON.

curl -H "Authorization: Bearer obs_xxxxxxxx" https://mx1.example.com/api/v1/stats?hours=24

Endpoints

GET /api/v1/health
Version and whether ClamAV answers.
GET /api/v1/stats?hours=24
{"hours": 24, "by_verdict": {"clean": 812, "spam": 40, ...}} (up to 90 days).
GET /api/v1/messages
Parameters: q (sender or subject text), verdict (comma list), hours (default 24), limit (default 100, max 1000). Returns id, times, direction, from, subject, score, verdict, action, reason, Message-ID, recipients with status, and the AI verdict.
GET /api/v1/messages/{id}
One message including every scan stage's details.
POST /api/v1/messages/{id}/release
Release to all quarantined recipients, or JSON body {"rcpts": ["user@contoso.com"]}. Returns {"ok": true, "info": "..."}.
POST /api/v1/messages/{id}/delete
Delete from quarantine.
POST /api/v1/lists
JSON {"scope": "org", "org_id": 3, "kind": "block", "pattern": "@bad.example", "note": "ticket 1234"}. Organization tokens use their own organization; global needs an all-organizations token.

Secure messages (encrypted to anyone)

For a mail client that sends a message encrypted for any recipient. The client encrypts the message itself (AES-256-GCM, a new key per message; envelope base64(iv[12] || ciphertext || tag), additional data obsidian-secure-v1) and puts the key in the link's #k= fragment, which browsers never send to the server - Obsidian Email Security stores the message without being able to read it. The recipient opens the link, proves their address with an emailed one-time code, and reads (and can answer) it in the browser. Off with Settings › Secure messages.

POST /api/v1/secure-messages
JSON {"sender", "sender_name", "recipients": [...], "ciphertext", "expires_days"} (up to 25 MB, 50 recipients, secure.max_days). Returns id, link (append #k=<key>), expiry.
GET /api/v1/secure-messages/{id}
Status: expiry, withdrawn, who opened it when, number of replies.
GET /api/v1/secure-messages/{id}/replies
The recipients' answers, encrypted with the message's key: [{"id", "from", "created_at", "ciphertext"}].
DELETE /api/v1/secure-messages/{id}
Withdraw: nobody can open it any more; its data is deleted.

Connected apps (a user's own quarantine)

Obsidian Mail connects with the user's own sign-in instead of an API token (see User portal › Connected apps). The app opens /app/connect?redirect_uri=&state=&code_challenge=&code_challenge_method=S256&mailbox=&device=&app= in the browser; redirect_uri must be http://127.0.0.1:<port>/<path> or http://localhost:<port>/<path> (anything else is a 400 page, never a redirect). mailbox is the address of the account the app has loaded and is required (apps that send none get a 400 page asking to update); Allow is offered only when it is one of the signed-in user's addresses (case does not matter), and the token stops working when it no longer is. After Allow the browser returns code + state (or error=access_denied). Tokens start with oma_, see only that user's mail, are not limited by plan (like the portal) and stop after 180 days unused.

POST /api/v1/app/token
{"code", "code_verifier"} → {"token", "email", "display_name", "addresses", "mailbox", "portal"}. One use, 5 minutes; otherwise 400 invalid_grant.
GET /api/v1/me
email, display name, addresses, mailbox (the one this token was connected for), version, brand. A token from 0.1.30 (no mailbox) gets 401 reconnect.
GET /api/v1/me/quarantine?limit=200
{"items", "count"}, newest first, max 500: id, received_at, from, from_name, mail_from, subject, verdict, score, reason, size, status, rcpts (the user's own), admin_only, can_release, has_raw.
GET /api/v1/me/quarantine/{id}/raw
The message (message/rfc822); 403 for admin-review mail, 410 when no longer kept, 404 when not the user's.
POST /api/v1/me/quarantine/{id}/release
{"allow_sender": bool}; 403 when the policy's user_release does not allow it (same rule as the portal).
POST /api/v1/me/quarantine/{id}/delete
Delete for the user's own addresses.
POST /api/v1/me/quarantine/{id}/report
{"label": "spam"|"ham", "block_sender": bool}.
DELETE /api/v1/me/token
The app disconnects itself (204).

Errors return a JSON {"error": "...", "detail": "..."} (same text) with status 400, 401 (bad or missing token), 403 (out of scope) or 404.

The machine-readable schema is at /api/openapi.json.