← Home

API reference

Base URL https://api.inboxar.com. JSON in, JSON out. All object ids are UUIDs. Invite-only beta: endpoints may still change; breaking changes will be announced to tenants.

Authentication

Two credentials work against /v1/*:

curl https://api.inboxar.com/v1/me -H "Authorization: Bearer ibx_XXXXXXXX_…"
{ "tenant": { "id": "…", "slug": "acme", "name": "Acme", "status": "active",
             "daily_send_limit": 5000, "mailbox_daily_send_limit": 500 },
  "actor": { "kind": "api_key", "api_key_id": "…" },
  "mail_host": "mx1.inboxar.com" }

Domains

MethodPathWhat
GET/v1/domainsList domains
POST/v1/domainsAdd a domain: {"name":"acme.com"} → 201 with DNS records
GET/v1/domains/:idDomain with its DNS records
POST/v1/domains/:id/verifyCheck MX, DKIM, SPF, DMARC over DNS; sets status to verified or pending
DELETE/v1/domains/:idRemove the domain and its mailboxes

Records to publish (returned by POST/GET): MX 10 mx1.inboxar.com, two DKIM TXTs (Ed25519 + RSA, rotating selectors), TXT v=spf1 mx -all (add your other senders if any), _dmarc TXT v=DMARC1; p=none; rua=mailto:dmarc@inboxar.com, plus autoconfig/autodiscover CNAMEs for mail clients. Aggregate DMARC reports land in /v1/dmarc.

Mailboxes

MethodPathWhat
GET/v1/mailboxesList
POST/v1/mailboxes{"domain_id","local_part","kind":"user"|"group","password"?}. Users get a generated password returned once when none is given; groups are shared mailboxes without a login.
GET/v1/mailboxes/:idOne mailbox
PATCH/v1/mailboxes/:id{"is_admin":true} — let this user manage the tenant from the web client
POST/v1/mailboxes/:id/passwordSet ({"password"}) or generate a new password
DELETE/v1/mailboxes/:idDelete the mailbox and its mail
GET/v1/mailboxes/:id/membersMembers of a group
POST/v1/mailboxes/:id/members{"mailbox_id"} — add a user to a shared mailbox; it appears as a second account in their client
DELETE/v1/mailboxes/:id/members/:mailbox_idRemove a member

Sending — POST /v1/messages

Sends from one of the tenant's mailboxes. The message is submitted through the mailbox itself (it shows up in its Sent folder), signed with your domain's DKIM, and queued. Delivery outcome arrives as message.delivered / message.bounced / message.deferred events.

curl https://api.inboxar.com/v1/messages \
  -H "Authorization: Bearer ibx_…" -H "Content-Type: application/json" \
  -d '{
    "from": {"email": "hello@acme.com", "name": "Acme"},
    "to": ["ann@example.org", {"email": "bob@example.org", "name": "Bob"}],
    "cc": [], "bcc": [], "reply_to": ["support@acme.com"],
    "subject": "Your account is ready",
    "text": "Welcome aboard.",
    "html": "<p>Welcome aboard.</p>",
    "in_reply_to": "<prev-message-id@example.org>",
    "headers": {"X-Campaign": "welcome"}
  }'
202 { "id": "…", "status": "queued", "message_id": "<…@acme.com>", "from": "hello@acme.com", "to": ["ann@example.org","bob@example.org"], "created_at": "…" }

Webhooks

MethodPathWhat
GET/v1/webhooksList
POST/v1/webhooks{"url":"https://…","events":["message.received","message.bounced"]} → 201 with secret (shown once). https only.
DELETE/v1/webhooks/:idDelete
GET/v1/webhooks/:id/deliveriesRecent delivery attempts and statuses

Each delivery is a POST with headers X-Inboxar-Event, X-Inboxar-Delivery and X-Inboxar-Signature: sha256=<hex> — HMAC-SHA256 of the raw body with the webhook secret. Respond 2xx within 10 s. Retries: 1 m, 5 m, 30 m, 2 h, 12 h, 24 h; then the delivery is marked failed. Deliveries are at-least-once — dedupe on id.

// Node: verify the signature
import { createHmac, timingSafeEqual } from "node:crypto";
const expected = "sha256=" + createHmac("sha256", process.env.WEBHOOK_SECRET).update(rawBody).digest("hex");
const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers["x-inboxar-signature"] ?? ""));

Payload

{
  "id": "…",                        // event id (UUID)
  "type": "message.received",
  "created_at": "2026-09-17T10:00:00.000Z",
  "data": {
    "address": "support@acme.com",  // recipient address the event is about (may be an alias or a catch-all address)
    "mailbox": "support@acme.com",  // message.received only — the mailbox it was delivered to
    "via": "mailbox",               // message.received only — mailbox | alias | catch_all
    "folder": "acme-site.com",      // message.received only — shared mailbox: the per-domain folder the message was filed into (alias/catch-all mail)
    "domain": "acme.com",
    "occurred_at": "…",
    "spam": false,                  // message.received only
    "message": {                    // message.received only — the parsed email
      "id": "…", "blob_id": "…", "thread_id": "…",
      "message_id": ["<…>"], "in_reply_to": [], "references": [],
      "received_at": "…", "sent_at": "…",
      "from": [{"email":"ann@example.org","name":"Ann"}], "to": [...], "cc": [...], "reply_to": [...],
      "subject": "Order #1", "text": "…", "html": "…",
      "text_truncated": false, "html_truncated": false,   // bodies capped at 512 KB
      "has_attachment": true,
      "attachments": [{"name":"inv.pdf","type":"application/pdf","size":100,"blob_id":"…","cid":null}],
      "headers": [{"name":"List-Unsubscribe","value":"…"}]
    },
    "stalwart": { … }               // raw server event, for debugging
  }
}

Attachment contents are fetched through JMAP with the mailbox's own credentials (Mail protocols) using blob_id.

Events

TypeMeaning
message.receivedDelivered into one of your mailboxes (inbox or spam — see data.spam)
message.deliveredOutbound message accepted by the remote server
message.bouncedPermanent failure
message.deferredTemporary failure, will retry
message.rejectedRejected at SMTP time (unknown recipient, policy)
auth.failedFailed login to one of your mailboxes

GET /v1/events?type=…&limit=… lists recent events regardless of webhooks.

DMARC

Every domain's DMARC record points aggregate reports at us; we parse them for you.

MethodPathWhat
GET/v1/dmarc?days=30&domain=Per-domain totals (messages, pass, fail) and top sending sources by IP
GET/v1/dmarc/reports?days=&domain=Individual reports (reporter, period, published policy)
GET/v1/dmarc/reports/:idOne report with its per-source records and auth results

API keys

MethodPathWhat
GET/v1/api-keysList (prefix, label, last used)
POST/v1/api-keys{"label"} → 201 with token (shown once). Max 20 active.
DELETE/v1/api-keys/:idRevoke

Mail protocols

ProtocolHostPortNotes
IMAPmx1.inboxar.com993SSL/TLS, login = full address
SMTP submissionmx1.inboxar.com587STARTTLS, same login
JMAPhttps://mx1.inboxar.com/.well-known/jmap443Basic or OAuth bearer; full RFC 8620/8621
Autoconfigautoconfig.<your-domain>443Thunderbird/Apple Mail/Outlook pick settings up automatically once the CNAMEs are set

Web client: app.inboxar.com — sign in with the mailbox address and password. Under Account & security each person can turn on two-factor authentication (TOTP) and create app passwords for mail apps; with two-factor on, IMAP/SMTP/JMAP accept only app passwords.

Web app, push notifications, diagnostics

app.inboxar.com is an installable web app (PWA): on iPhone use Share → Add to Home Screen, on Android/desktop Chrome use the install prompt in the address bar. Push notifications for new mail are per device and per mailbox; turn them on under Notifications & diagnostics in the sidebar (iPhone: only from the installed app). Shared mailboxes notify every member; spam never pushes.

MethodPathWhat
GET/public/push/configPublic VAPID key (applicationServerKey)
POST/v1/team/push/subscribeMailbox token: {"subscription": PushSubscription.toJSON(), "ua"} → device id. One browser endpoint belongs to one mailbox.
GET/v1/team/push/devicesDevices of the signed-in mailbox (last push, last error)
POST/v1/team/push/testTest push to every device of the caller — returns the push service's verbatim answer per device
DELETE/v1/team/push/devices/:idUnsubscribe a device
POST/public/diag/logDiagnostic log from the app (what happened and how fast, no message content). Sent by the person from the Diagnostics screen or automatically after an error; answers with a short id to quote to support. Kept 30 days.

Workspaces (owner sign-in)

A workspace is created at app.inboxar.com/signup with any email — a one-time link confirms it, no password. One email can own several workspaces; switch between them in the Admin sidebar or create another one there without a new confirmation. New workspaces are approved by hand during the beta.

MethodPathWhat
POST/public/signup{"name", "email"} → confirmation link by email
POST/public/login{"email"} → sign-in link (lands in the last used workspace)
POST/public/verify{"token"} → owner session own_… (30 days), usable as Bearer on /v1/* with admin rights
GET/public/workspacesOwner session: all workspaces of this email, current one marked
POST/public/workspaces{"name"} → another workspace for the same email (up to 3 awaiting approval, 10 total)
POST/public/switch{"tenant_id"} → new session for that workspace; the old one is revoked
POST/public/logoutRevoke the session

Errors & limits

Errors are {"error": "code", "detail": "…"} with a matching HTTP status: 400 validation, 401 bad credentials, 403 not allowed (suspended tenant, foreign mailbox), 404, 409 conflict (name taken, already member), 413 body too large, 429 daily_limit_reached, 502 mail server refused, 503 not_configured. Every response carries X-Request-Id — include it when writing to hello@inboxar.com.