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;globalneeds 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 400invalid_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.