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/*:
- API key —
Authorization: Bearer ibx_…. Created by an operator at onboarding or by a tenant admin (API keys). Keys are shown once and stored hashed. - Mailbox login (OAuth) — the access token a user gets from the mail server's OAuth flow (used by the web client). Accepted only for mailboxes flagged
is_admin. Discovery:GET /auth/config(public).
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
| Method | Path | What |
|---|---|---|
| GET | /v1/domains | List domains |
| POST | /v1/domains | Add a domain: {"name":"acme.com"} → 201 with DNS records |
| GET | /v1/domains/:id | Domain with its DNS records |
| POST | /v1/domains/:id/verify | Check MX, DKIM, SPF, DMARC over DNS; sets status to verified or pending |
| DELETE | /v1/domains/:id | Remove 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
| Method | Path | What |
|---|---|---|
| GET | /v1/mailboxes | List |
| 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/:id | One mailbox |
| PATCH | /v1/mailboxes/:id | {"is_admin":true} — let this user manage the tenant from the web client |
| POST | /v1/mailboxes/:id/password | Set ({"password"}) or generate a new password |
| DELETE | /v1/mailboxes/:id | Delete the mailbox and its mail |
| GET | /v1/mailboxes/:id/members | Members 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_id | Remove 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": "…" }
frommust be an active user mailbox of the tenant (groups cannot send). Up to 50 recipients across to/cc/bcc.textand/orhtmlrequired. OnlyX-*custom headers.- Limits: per tenant and per mailbox per rolling 24 h (see
/v1/me). Over the limit →429 daily_limit_reached. GET /v1/messages,GET /v1/messages/:id— the send log (no bodies).
Webhooks
| Method | Path | What |
|---|---|---|
| GET | /v1/webhooks | List |
| POST | /v1/webhooks | {"url":"https://…","events":["message.received","message.bounced"]} → 201 with secret (shown once). https only. |
| DELETE | /v1/webhooks/:id | Delete |
| GET | /v1/webhooks/:id/deliveries | Recent 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", // mailbox the event is about
"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
| Type | Meaning |
|---|---|
| message.received | Delivered into one of your mailboxes (inbox or spam — see data.spam) |
| message.delivered | Outbound message accepted by the remote server |
| message.bounced | Permanent failure |
| message.deferred | Temporary failure, will retry |
| message.rejected | Rejected at SMTP time (unknown recipient, policy) |
| auth.failed | Failed 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.
| Method | Path | What |
|---|---|---|
| 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/:id | One report with its per-source records and auth results |
API keys
| Method | Path | What |
|---|---|---|
| GET | /v1/api-keys | List (prefix, label, last used) |
| POST | /v1/api-keys | {"label"} → 201 with token (shown once). Max 20 active. |
| DELETE | /v1/api-keys/:id | Revoke |
Mail protocols
| Protocol | Host | Port | Notes |
|---|---|---|---|
| IMAP | mx1.inboxar.com | 993 | SSL/TLS, login = full address |
| SMTP submission | mx1.inboxar.com | 587 | STARTTLS, same login |
| JMAP | https://mx1.inboxar.com/.well-known/jmap | 443 | Basic or OAuth bearer; full RFC 8620/8621 |
| Autoconfig | autoconfig.<your-domain> | 443 | Thunderbird/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.
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.