The Mailbox API lets you see and act on the email activity of your sending.ac mailboxes. You can list the emails your mailboxes send and receive, get notified in real time through webhooks, and send or read mail through the Microsoft Graph endpoints you already know.
This is an early preview (v1alpha1). Endpoints and fields can change. We will tell you before we make breaking changes.
Base URL
https://api.customers.ac/api/mailbox/v1alpha1Authentication
Every request needs your API key as a bearer token:
Authorization: Bearer sk_sandbox_xxxxxxxxCreate a key from your dashboard at api.customers.ac, in the Mailbox API credentials section. You can create sandbox keys (sk_sandbox_...) for testing and production keys (sk_live_...) for live traffic. Both return your real data and use the same base URL. Keep your key secret. If it leaks, revoke it from the dashboard and create a new one. For an interactive reference, open the Mailbox API Reference page in the dashboard.
List email events
GET /eventsReturns the individual emails your mailboxes sent and received, newest first, with aggregate counts included in the same response. This is the endpoint to poll when you are not using webhooks.
Time range
from and to accept either an absolute time (RFC 3339 or a Unix timestamp) or a relative expression (now-24h, now-7d, now). The default range is the last 24 hours. The maximum range is 7 days.
?from=now-24h&to=now
?from=2026-07-09T00:00:00Z&to=2026-07-10T00:00:00ZFilters
Parameter | Values |
filter[direction] | inbound or outbound |
filter[classification] | one or more of the values below, comma separated |
filter[domains] | one or more domain names or domain ids, comma separated |
Classifications:
Value | Meaning |
warmup | Automated warmup message. |
warmup_reply | A reply inside the warmup network. |
outreach | A message from your outreach campaigns. |
reply | A genuine reply from a recipient. |
other_inbound | Other received mail. |
soft_bounce | Temporary delivery failure. |
hard_bounce | Permanent delivery failure. |
autoresponder | Automatic reply, such as out of office. |
system | System or postmaster message. |
placement_test | Inbox placement test message. |
Pagination
Parameter | Meaning |
page[size] | Results per page. Default 25, maximum 100. |
page[after] | A cursor from a previous response. |
order | desc (newest first, default) or asc (oldest first). |
Example
curl "https://api.customers.ac/api/mailbox/v1alpha1/events?from=now-24h&filter[direction]=inbound&filter[classification]=reply&filter[domains]=example.com,acme.io&page[size]=25" \
-H "Authorization: Bearer sk_sandbox_xxxxxxxx"Response
{
"data": [
{
"id": "evt_01J...",
"type": "email.received",
"direction": "inbound",
"classification": "reply",
"occurred_at": "2026-07-10T01:22:05Z",
"domain": { "id": "019d...", "name": "example.com" },
"mailbox": "alice@example.com",
"message": {
"internet_message_id": "<CAB...@mail.gmail.com>",
"subject": "Re: your note",
"from": "lead@prospect.com",
"to": ["alice@example.com"],
"received_at": "2026-07-10T01:22:03Z",
"body": { "content_type": "html", "content": "<the full message body>" }
},
"cursor": "eyJ0IjoiMjAyNi0..."
}
],
"summary": {
"window": { "from": "2026-07-09T01:23:00Z", "to": "2026-07-10T01:23:00Z" },
"totals": { "inbound": 812, "outbound": 3140 },
"by_classification": { "warmup": 3020, "reply": 34, "hard_bounce": 12, "outreach": 61 },
"by_domain": [
{ "id": "019d...", "domain": "example.com", "inbound": 812, "outbound": 3140 }
]
},
"links": { "self": "https://api.customers.ac/api/mailbox/v1alpha1/events?from=2026-07-09T01:23:00Z&to=2026-07-10T01:23:00Z" },
"pagination": { "has_more": true, "next_cursor": "eyJ0IjoiMjAyNi0..." }
}The full message body is included in every event. summary counts the full filtered range, not just the current page.
Poll for new events
To watch for new email, do not re-request a moving time window. Instead, page forward from the last event you saw:
- Request with
order=asc.
- Store the
next_cursorfrom the response.
- On the next poll, pass it as
page[after]. You receive only events newer than that cursor.
curl "https://api.customers.ac/api/mailbox/v1alpha1/events?order=asc&page[after]=eyJ0IjoiMjAyNi0..." \
-H "Authorization: Bearer sk_sandbox_xxxxxxxx"Poll about once per minute. This path is deterministic and cache friendly, so it stays fast and does not count against you as heavily.
Webhooks
Register a URL and we call it when a matching event happens on your domains. This is the preferred way to react to received email.
Register
curl -X POST https://api.customers.ac/api/mailbox/v1alpha1/webhooks \
-H "Authorization: Bearer sk_sandbox_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/sending",
"domains": ["example.com", "acme.io"],
"events": ["email.received", "email.sent"],
"secret": "whsec_your_shared_secret"
}'Field | Meaning |
url | Where we send the POST. |
domains | One or more domain names or ids to watch. |
events | email.received, email.sent, or both. |
secret | Used to sign each delivery. If you omit it, we generate one and return it once. |
Manage subscriptions with GET /webhooks, GET /webhooks/{id}, and DELETE /webhooks/{id}.
Delivery payload
We POST a body that wraps the same event object the events endpoint returns:
{
"id": "whd_01J...",
"type": "email.received",
"created_at": "2026-07-10T01:22:06Z",
"data": { "id": "evt_01J...", "type": "email.received", "direction": "inbound", "...": "same shape as an /events item" }
}Because the data object matches an /events item, one handler can process both webhook deliveries and polled events.
Verify the signature
Every delivery includes an X-Sending-Signature header:
X-Sending-Signature: sha256=<hex hmac of the raw request body, keyed with your secret>Compute the HMAC SHA-256 of the raw body with your secret and compare it in constant time. Reject the request if it does not match.
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const expected = "sha256=" +
crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}Send and read mail
The Mailbox API exposes the Microsoft Graph endpoints you need to send and read mail on your mailboxes. Point your existing Microsoft Graph client at our base URL and use your sending.ac key as a static token. Your code stays the same.
Base URL: https://api.customers.ac/api/mailbox/v1alpha1/azure/v1.0
Auth: Authorization: Bearer sk_sandbox_xxxxxxxx
You change two things: the base URL, and the credential (your sending.ac key instead of a Microsoft token). Paths, query parameters, and request and response bodies are native Microsoft Graph.
Supported endpoints:
Method | Path |
POST | /users/{email}/sendMail |
GET | /users/{email}/messages (supports $select, $filter, $top) |
GET | /users/{email}/messages/{id} |
GET | /users/{email}/mailFolders |
You can only address mailboxes that belong to your account. Other Graph paths return 404.
curl
curl -X POST \
https://api.customers.ac/api/mailbox/v1alpha1/azure/v1.0/users/alice@acme.com/sendMail \
-H "Authorization: Bearer sk_sandbox_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"message":{"subject":"Hi","body":{"contentType":"Text","content":"hello"},
"toRecipients":[{"emailAddress":{"address":"lead@example.com"}}]}}'Microsoft Graph JavaScript SDK
import { Client } from "@microsoft/microsoft-graph-client";
const client = Client.init({
baseURL: "https://api.customers.ac/api/mailbox/v1alpha1/azure/v1.0",
authProvider: (done) => done(null, "sk_sandbox_xxxxxxxx"),
});
await client.api("/users/alice@acme.com/sendMail").post({ message: { /* ... */ } });
const inbox = await client.api("/users/alice@acme.com/messages")
.select("subject,from,receivedDateTime").top(25).get();Microsoft Graph .NET SDK
var http = new HttpClient {
BaseAddress = new Uri("https://api.customers.ac/api/mailbox/v1alpha1/azure/v1.0")
};
var graph = new GraphServiceClient(http, new StaticTokenProvider("sk_sandbox_xxxxxxxx"));
await graph.Users["alice@acme.com"].SendMail.PostAsync(new() { Message = msg });Gmail support is planned under a separate /google/ prefix and is not available yet.
Rate limits and caching
The events endpoint allows 10 requests per minute per key. If you exceed it you get 429 with a Retry-After header. Wait for the number of seconds in that header and try again.
Responses can be reused for up to a minute. Each response carries an ETag and a Cache-Control header. Send the ETag back on your next request as If-None-Match. If nothing changed, you get 304 Not Modified with no body, which is faster and cheaper for both sides. For live updates, prefer the cursor polling described above over re-querying a moving window.
Errors
Errors use a consistent JSON shape:
{ "error": { "code": "rate.quota_exceeded", "message": "Too many requests. Retry after the number of seconds in Retry-After." } }Status | Meaning |
401 | Missing or invalid API key. |
403 | The key is valid but not allowed to access this resource. |
404 | Unknown mailbox, path, or resource. |
422 | A parameter failed validation. The response lists the fields. |
429 | Rate limit exceeded. See Retry-After. |
Support
If something is not working or you need access you do not have, contact your Deliverability Advisor.
