This is the abridged developer documentation for builders.ac # Build on sending.ac > API reference and guides for the sending.ac Mailbox API and Provisioning API. Two APIs. One sends and reads mail from mailboxes you already own; the other creates mailboxes for your own customers. They take different keys, and mixing them up is the most common first failure — so every page here says which one it belongs to. ## Send and read mail [Section titled “Send and read mail”](#send-and-read-mail) Point an unmodified Microsoft Graph SDK at our base URL. Your client, your code, your error handling — you change two values and nothing else. [Mailbox API](/api/mailbox/)v1alpha1Unstable * Base URL `https://api.customers.ac/api/mailbox/v1alpha1` * Key `sk_live_…`Scope mailbox, live only. Sandbox keys are refused. [Guide](/guides/mailbox-api/) · [Reference](/api/mailbox/) · 4 operations ## Provision mailboxes [Section titled “Provision mailboxes”](#provision-mailboxes) Create users, senders and domains for your end customers. The platform handles Microsoft 365 tenant setup, DNS and mailbox creation, and every write returns an operation you poll. [Provisioning API](/api/provisioning/)v1Internal testing * Base URL `https://live-api.customers.ac/v1` * Key `sac_live_… / sac_test_…`Scoped per operation. Sandbox provisions nothing. [Guide](/guides/provisioning-flow/) · [Reference](/api/provisioning/) · 17 operations ## Not sure which key you have? [Section titled “Not sure which key you have?”](#not-sure-which-key-you-have) [Authentication](/guides/authentication/) puts both key formats side by side and explains why the wrong one returns `403` rather than `401`. ## For agents [Section titled “For agents”](#for-agents) The whole site is available as plain text, and both OpenAPI documents are served directly. | | | | ------------------------------------------------------ | -------------------------------------------- | | [`/llms.txt`](/llms.txt) | index | | [`/llms-full.txt`](/llms-full.txt) | everything, including the complete reference | | [`/llms-small.txt`](/llms-small.txt) | prose only, for small context windows | | [`/specs/mailbox.json`](/specs/mailbox.json) | Mailbox API, OpenAPI 3.1 | | [`/specs/provisioning.json`](/specs/provisioning.json) | Provisioning API, OpenAPI 3.1 | # Guides > Task-oriented guides for the sending.ac Mailbox API and Provisioning API. sending.ac exposes two APIs. They serve different jobs and use different keys. ## Mailbox API [Section titled “Mailbox API”](#mailbox-api) Send and read mail from mailboxes you already own, using an **unmodified Microsoft Graph SDK**. Point your existing Graph client at our base URL, authenticate with a sending.ac key, and the rest of your code is unchanged. * [Send and read mail](/guides/mailbox-api/) — setup, sending, reading, paging, errors * [Reference](/api/mailbox/) — every operation ## Provisioning API [Section titled “Provisioning API”](#provisioning-api) Create users, senders, domains and mailboxes on behalf of your own customers. The platform handles Microsoft 365 tenant setup, DNS and mailbox creation. * [Provision mailboxes](/guides/provisioning-flow/) — the object model and the async path * [Reference](/api/provisioning/) — every operation Still in internal testing; access is granted case by case. ## Both [Section titled “Both”](#both) * [Authentication](/guides/authentication/) — the two key types, and why the wrong one returns `403` rather than `401` The raw OpenAPI documents are served at [`/specs/mailbox.json`](/specs/mailbox.json) and [`/specs/provisioning.json`](/specs/provisioning.json). # Authentication > The two sending.ac key types, how they differ, and why one is refused where the other works. sending.ac has two APIs and two kinds of key. They are not interchangeable, and using the wrong one is the most common first failure — it returns `403`, not `401`, because the key itself is valid. ## The two key types [Section titled “The two key types”](#the-two-key-types) | | Mailbox API | Provisioning API | | ------------ | ----------------------------------------------- | ---------------------------------- | | Prefix | `sk_live_…` | `sac_live_…` / `sac_test_…` | | Scope | `mailbox` | scoped per operation | | Environments | **live only** | live and sandbox | | Issued from | Production Credentials in the customer portal | the sending.ac partner dashboard | | Base URL | `https://api.customers.ac/api/mailbox/v1alpha1` | `https://live-api.customers.ac/v1` | Both are sent the same way: ```text Authorization: Bearer ``` ## Mailbox API keys [Section titled “Mailbox API keys”](#mailbox-api-keys) [Mailbox API](/api/mailbox/)v1alpha1Unstable * Base URL `https://api.customers.ac/api/mailbox/v1alpha1` * Key `sk_live_…`Scope mailbox, live only. Sandbox keys are refused. A Mailbox key must be **scope `mailbox`** and **environment `live`**. Both are enforced: * a Provisioning key returns `403` / `ErrorAccessDenied` * a sandbox key returns `403` / `ErrorAccessDenied` Sandbox keys are refused by design rather than downgraded to a mock. Every call touches a real Microsoft 365 mailbox, and `sendMail` delivers real mail to real recipients, so there is nothing sandbox-like to offer. Errors on this API use [Microsoft’s OData error shape](/api/mailbox/operations/sendmail/), not a sending.ac envelope, so a Graph SDK deserialises them as ordinary `GraphError`s. ## Provisioning API keys [Section titled “Provisioning API keys”](#provisioning-api-keys) [Provisioning API](/api/provisioning/)v1Internal testing * Base URL `https://live-api.customers.ac/v1` * Key `sac_live_… / sac_test_…`Scoped per operation. Sandbox provisions nothing. Provisioning tokens are scoped per operation. A request that needs a scope your token does not carry returns `403 auth.insufficient_scope`. Live keys begin with `sac_live_`, sandbox keys with `sac_test_`. Unlike the Mailbox API, sandbox is meaningful here: no real infrastructure is provisioned and operations complete instantly, which makes it useful for exercising the async flow without waiting on M365. Internal testing — not generally available The Provisioning API is still in internal testing. Endpoints and payloads may change without notice, and access is granted case by case. Treat the reference as a preview rather than a contract, and talk to us before building against it. ## Rate limits [Section titled “Rate limits”](#rate-limits) | API | Limit | On exceeding | | ------------ | ----------------------------- | ------------------------------------------------------------------------------------------- | | Mailbox | 60 requests/minute per key | `429` with `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` | | Provisioning | 120 requests/minute per token | `429 rate.quota_exceeded` with `Retry-After` | ## Which one do I want? [Section titled “Which one do I want?”](#which-one-do-i-want) * Sending or reading mail from a mailbox that already exists → **Mailbox API**. * Creating users, senders, domains or mailboxes on behalf of your own customers → **Provisioning API**. Most integrations that do both hold one of each. # Send and read mail > Send and read mail from your sending.ac mailboxes with an unmodified Microsoft Graph SDK. The Mailbox API lets you send and read mail from the mailboxes you own on sending.ac. It speaks Microsoft Graph, so if you already have a Graph client you keep it — you change the base URL and the credential, and nothing else. [Mailbox API](/api/mailbox/)v1alpha1Unstable * Base URL `https://api.customers.ac/api/mailbox/v1alpha1` * Key `sk_live_…`Scope mailbox, live only. Sandbox keys are refused. ## Before you start [Section titled “Before you start”](#before-you-start) You need a **Mailbox** key in the **live** environment. Create one under Production Credentials, choosing the Mailbox scope. Sandbox keys are refused These calls send and read real mail from real mailboxes, so there is no sandbox behaviour to offer. A sandbox key — or a Provisioning key — gets `403`. ## Base URL and auth [Section titled “Base URL and auth”](#base-url-and-auth) Every request goes to: ```text https://api.customers.ac/api/mailbox/v1alpha1 ``` The Graph surface lives under `/azure/v1.0`. Authenticate with your key as a bearer token: ```text Authorization: Bearer sk_live_xxxxxxxx ``` ## Point your client at us [Section titled “Point your client at us”](#point-your-client-at-us) Most Graph SDKs only attach the bearer token to hosts they trust. Pointed at another base URL they send the request unauthenticated, and you get a `401` that looks like a bad key. Tell the SDK to trust our host: ```javascript import { Client } from '@microsoft/microsoft-graph-client'; const client = Client.init({ baseUrl: 'https://api.customers.ac/api/mailbox/v1alpha1/azure', defaultVersion: 'v1.0', authProvider: (done) => done(null, 'sk_live_xxxxxxxx'), // Required. Without this the SDK omits your key and every call returns 401. customHosts: new Set(['api.customers.ac']), }); ``` ```csharp var http = new HttpClient { BaseAddress = new Uri("https://api.customers.ac/api/mailbox/v1alpha1/azure/v1.0") }; // Allow the credential to be sent to our host, as customHosts does in the JS SDK. var graph = new GraphServiceClient(http, new StaticTokenProvider("sk_live_xxxxxxxx")); ``` ## Send an email [Section titled “Send an email”](#send-an-email) `sendMail` takes Graph’s own payload and returns `202 Accepted` with an empty body once Microsoft accepts the message. Full request schema in [the reference](/api/mailbox/operations/sendmail/). ```bash curl -X POST \ https://api.customers.ac/api/mailbox/v1alpha1/azure/v1.0/users/alice@acme.com/sendMail \ -H "Authorization: Bearer sk_live_xxxxxxxx" \ -H "Content-Type: application/json" \ -d '{"message":{"subject":"Hi","body":{"contentType":"Text","content":"hello"}, "toRecipients":[{"emailAddress":{"address":"lead@example.com"}}]}}' ``` ```javascript await client.api('/users/alice@acme.com/sendMail').post({ message: { subject: 'Hi', body: { contentType: 'Text', content: 'hello' }, toRecipients: [{ emailAddress: { address: 'lead@example.com' } }], }, saveToSentItems: true, }); ``` Never retry a send automatically `202` means accepted for delivery, not delivered. If a send fails at the transport layer we return `502` and do **not** retry, because Microsoft may already have accepted and sent the message. Treat a `502` here as *unknown*, not *failed*, and only retry if sending twice is acceptable. ## Read mail [Section titled “Read mail”](#read-mail) List messages with the OData parameters you already use — `$select`, `$filter`, `$top`, `$orderby` — forwarded to Microsoft untouched. ```javascript const inbox = await client .api('/users/alice@acme.com/messages') .select('id,subject,from,receivedDateTime') .top(25) .get(); ``` ```bash curl -G \ https://api.customers.ac/api/mailbox/v1alpha1/azure/v1.0/users/alice@acme.com/messages \ -H "Authorization: Bearer sk_live_xxxxxxxx" \ --data-urlencode '$select=id,subject,from,receivedDateTime' \ --data-urlencode '$top=25' ``` Always send `$select`. The default projection is large, and message bodies dominate the response. Every supported parameter is listed under [List messages](/api/mailbox/operations/listmessages/). ## Paging [Section titled “Paging”](#paging) We rewrite `@odata.nextLink` to point back at us, so page iterators work unchanged and you never receive a raw `graph.microsoft.com` link that your key could not authenticate against. ```javascript import { PageIterator } from '@microsoft/microsoft-graph-client'; const first = await client.api('/users/alice@acme.com/messages').top(25).get(); await new PageIterator(client, first, (message) => { console.log(message.subject); return true; }).iterate(); ``` ## What you can call [Section titled “What you can call”](#what-you-can-call) The proxy runs a strict allow-list. These four routes are reachable; anything else returns `404` even if it exists in Graph. | Method | Path | | ------ | ------------------------------------------------------------------------ | | `POST` | [`/users/{email}/sendMail`](/api/mailbox/operations/sendmail/) | | `GET` | [`/users/{email}/messages`](/api/mailbox/operations/listmessages/) | | `GET` | [`/users/{email}/messages/{id}`](/api/mailbox/operations/getmessage/) | | `GET` | [`/users/{email}/mailFolders`](/api/mailbox/operations/listmailfolders/) | You can only address mailboxes belonging to your account. A mailbox that is not yours returns `404` rather than `403`, so the API never confirms which addresses exist. A supported path with the wrong method returns `405`. ## Errors [Section titled “Errors”](#errors) Errors on the Graph surface come back in **Microsoft’s own format**, including errors we raise ourselves, so your existing Graph error handling keeps working. ```json { "error": { "code": "ResourceNotFound", "message": "No such mailbox.", "innerError": { "request-id": "…", "date": "…" } } } ``` | Status | Meaning | | ------ | --------------------------------------------------------------------------------- | | `401` | Missing or invalid API key, or the key was revoked. | | `403` | Valid key, but not a live Mailbox key — a Provisioning or sandbox key lands here. | | `404` | Unknown mailbox or path, including a mailbox that is not yours. | | `405` | Supported path, unsupported method. | | `413` | Request body above 10 MB. | | `429` | Rate limit exceeded. See `Retry-After`. | | `502` | We could not reach Microsoft. Ambiguous for sends. | | `503` | Temporarily unavailable. Safe to retry with backoff. | ## Rate limits [Section titled “Rate limits”](#rate-limits) 60 requests per minute per API key. Going over returns `429` with `Retry-After` and `X-RateLimit-*` headers — wait the number of seconds given and try again. Microsoft’s own throttling is passed through with its headers intact, so your SDK’s built-in backoff keeps working. ## Not available yet [Section titled “Not available yet”](#not-available-yet) Two things are planned and appear in the API reference so you can see them coming, but there is nothing to call today: * **Events and webhooks** — a feed of delivery and engagement activity across your mailboxes, plus push delivery. Until it ships, poll `messages` for the mailboxes you care about. Shape preview under [Events](/api/mailbox/operations/tags/events/). * **Gmail** — reserved under a `/google/` prefix, which answers `501` so you can detect it deliberately. Only Microsoft 365 mailboxes work today. v1alpha1 is unstable Endpoints, payloads and error codes can change without notice and without a version bump. Do not build anything you cannot redeploy. ## Getting help [Section titled “Getting help”](#getting-help) When something goes wrong, quote the `x-correlation-id` response header — the same value appears as `request-id` inside our error bodies. It identifies the exact request, including Microsoft’s own id from `x-ms-request-id`, which is what we need to escalate on your behalf. # Provision mailboxes > How a user becomes a sender, a connected domain and working mailboxes on the Provisioning API. Internal testing — not generally available The Provisioning API is still in internal testing. Endpoints and payloads may change without notice, and access is granted case by case. Treat this guide and the reference as a preview rather than a contract, and talk to us before building against it. The Provisioning API lets integration partners white-label mailbox provisioning for their own end customers. You create senders and connect domains; the platform handles Microsoft 365 tenant setup, DNS configuration and mailbox creation. [Provisioning API](/api/provisioning/)v1Internal testing * Base URL `https://live-api.customers.ac/v1` * Key `sac_live_… / sac_test_…`Scoped per operation. Sandbox provisions nothing. ## The object model [Section titled “The object model”](#the-object-model) | Object | What it is | | ------------- | ------------------------------------------------------------------------ | | **User** | an end customer on your platform | | **Sender** | a logical grouping of domains and mailboxes belonging to a user | | **Domain** | a domain connected to the platform | | **Mailbox** | an individual email account with IMAP/SMTP credentials | | **Operation** | every write returns an `operation_id` you can poll or receive by webhook | ## Everything is asynchronous [Section titled “Everything is asynchronous”](#everything-is-asynchronous) A `POST` that triggers infrastructure changes returns immediately with `status: "pending"` and an `operation_id`. It has not happened yet. Poll [`GET /operations/{id}`](/api/provisioning/operations/getoperation/) to track progress, or take the webhook. This is the part most integrations get wrong: treating the `201` as completion. The mailbox does not exist until the operation says so. ## The path through [Section titled “The path through”](#the-path-through) 1. **Create a user** — [`POST /users`](/api/provisioning/operations/createuser/) — one per end customer on your platform. 2. **Create a sender** — [`POST /senders`](/api/provisioning/operations/createsender/) — the grouping that domains and mailboxes hang off. 3. **Connect a domain.** The platform assigns nameservers in response. 4. **Update the registrar** with those nameservers, then wait for propagation — **up to 48 hours**. Nothing you can do shortens this. 5. **M365 is provisioned automatically** once propagation completes. You do not call anything to trigger it. 6. **Read the mailboxes** — [`GET /senders/{id}/mailboxes`](/api/provisioning/operations/listmailboxes/) — and their [credentials](/api/provisioning/operations/getmailboxcredentials/) when you need IMAP/SMTP details. Step 4 is the long pole and it is outside the API. Design your onboarding so a customer can leave and come back rather than watching a spinner. ## Pagination [Section titled “Pagination”](#pagination) List endpoints are cursor-based. Pass `page[size]` (max 100, default 25) and `page[after]`, an opaque cursor taken from a previous response. ## Tearing down [Section titled “Tearing down”](#tearing-down) Deprovisioning is explicit and separate per level: * [`DELETE /senders/{id}`](/api/provisioning/operations/deprovisionsender/) * [`DELETE /domains/{id}`](/api/provisioning/operations/deprovisiondomain/) Both are writes, so both return an `operation_id` and complete asynchronously like everything else. ## Sandbox [Section titled “Sandbox”](#sandbox) Sandbox keys (`sac_test_…`) run against `https://sandbox-api.customers.ac/v1`, where no real infrastructure is provisioned and operations complete instantly. That makes it the right place to exercise the async polling flow without waiting on DNS propagation or M365. ## Webhooks [Section titled “Webhooks”](#webhooks)