Mailboxes
Connect an existing Gmail or Outlook account over OAuth — from a hosted page, a plain link, or the API — keep it synced, and put your own brand on the consent screen.
A mailbox is an existing Gmail or Outlook account, connected once over OAuth. Its mail keeps flowing through Google or Microsoft exactly as before; AI Inbx syncs it, and can send and receive as that address.
That’s the difference from a domain, which needs DNS access and takes over mail for every address on it. A mailbox needs nothing but the account holder clicking approve, changes nothing about their existing setup, and covers one address.
Reach for it when the address already has an inbox somewhere — a rep’s work account, a founder’s inbox — and the mail should read as coming from that person.
Connecting a mailbox
Three ways in, ordered by how much of the flow you own. They all end the same way: the person approves at Google or Microsoft, the mailbox lands in your workspace, and mailbox.connected fires.
| You write | Your users see | Needs | |
|---|---|---|---|
| Hosted page | Nothing — share a link | A page in your brand, then “you’re connected” | Your own OAuth app |
| Connect link | A URL on a button | Only the provider’s consent screen, then your page | Your own OAuth app, plus a registered return URL |
| API | A server-side call per connection | Only the provider’s consent screen, then your page | A full API key |
Hosted page
Every OAuth app of yours has a page at its connect_url — https://aiinbx.com/connect/<slug> — wearing your name, logo, accent and support address, with nothing of AI Inbx on it. Send people there, or put it in an email; it’s a permanent link, not a one-time one.
https://aiinbx.com/connect/acme?ref=user_8812
ref?string
Your own identifier for the person, up to 200 characters. Echoed on mailbox.connected.
stringreturn_to?string
One of the app's registered return URLs. Adds a "Continue to <your app>" button to the page they end on, carrying the same query as a connect link does.
stringBrand the page from the app’s settings in the console, or through oauthApps.create — accent, tagline, support_email, logo_url.
Connect link
The same handoff with no page of ours in it. The person clicks a button on your site, lands on the consent screen, and comes back to a page of yours. Build the URL yourself; there’s nothing to call first:
https://aiinbx.com/connect/<slug>/start?return_to=<url>&ref=<id>
return_tostring
Where they land afterwards. Must match one of the app's return_urls on origin and path; the query is yours to fill.
stringref?string
Your own identifier for the person, up to 200 characters. Echoed on mailbox.connected.
stringRegister where the link may return to
A connect link is opened with no credential, so it will only send someone back to a page you’ve listed on the app — the same idea as the redirect URIs you register with Google. Add them in the console, or on the app:
await aiinbx.oauthApps.update(app.id, {
return_urls: [
"https://app.example.com/settings/mailboxes",
"http://localhost:3000/settings/mailboxes",
],
})Entries are matched on origin and path, so one covers every query string that page is opened with. https only, except on localhost. An unregistered return_to fails with return_url_not_registered before anyone reaches the provider.
Put the link on a button
const connectUrl = new URL(`https://aiinbx.com/connect/acme/start`)
connectUrl.searchParams.set("return_to", "https://app.example.com/settings/mailboxes")
connectUrl.searchParams.set("ref", user.id)
<a href={connectUrl.href}>Connect your Gmail</a><a href="https://aiinbx.com/connect/acme/start?return_to=https%3A%2F%2Fapp.example.com%2Fsettings%2Fmailboxes&ref=user_8812">
Connect your Gmail
</a>Read the outcome off the query
They come back to return_to with the result appended:
| Query | When | Value |
|---|---|---|
mailbox |
Connected | The new mailbox’s mbx_… id |
connected |
Connected | The address they authorized |
error |
Declined or failed | What went wrong, e.g. access_denied |
Fine for showing a “connected” state on the page. Not proof — see the last step.
Wait for mailbox.connected
The webhook is the authoritative signal, and carries your ref. The browser landing on return_to only says it came back; a person who closes the tab at the consent screen never triggers it.
if (event.type === "mailbox.connected") {
await markMailboxReady(event.data.ref, event.data.mailbox_id)
// `reconnected` is true when this replaced an existing authorization.
}API
For when your server should decide — the region, how much history to import, or the shared AI Inbx app rather than your own. The URL it returns is single-use and short-lived: redirect to it, don’t email it.
const { url } = await aiinbx.mailboxes.connect({
provider: "google",
return_to: "https://app.example.com/settings/mailboxes",
ref: "user_8812",
backfill_days: 30,
})
// Send the customer to `url`.
provider"google" | "microsoft"
Which provider to authorize against.
"google" | "microsoft"return_tostring
Where the customer lands after approving or declining. Any URL — the call is authenticated, so it isn't held to the app's return_urls.
stringref?string
Your own identifier, echoed back on mailbox.connected so you know which user finished.
stringapp_id?string
Use one of your own OAuth apps instead of the shared AI Inbx app — see below.
stringspace_id?string
The space the mailbox lands in, and with it everything synced from it. Omit for the workspace itself.
stringregion?"eu-central-1" | "us-east-1"
Where this mailbox's mail is processed and stored. Defaults to eu-central-1.
"eu-central-1" | "us-east-1"backfill_days?number
How much history to import on first sync, 0–90 days. Omit to start from the connection forward.
numberThe return carries the same query as a connect link, and the same rule applies: wait for mailbox.connected rather than trusting the redirect. A stale URL fails with expired_state. Creating connection URLs requires a full scope key.
space_id is the reason a platform uses this path: it is the only one of the three that can put the mailbox in a space. The hosted page and a connect link are opened with no credential, so they cannot take a space — anyone could file a mailbox into another customer’s. A mailbox lands in a space only through this call, which binds the space inside the encrypted OAuth state of the URL it returns. So a platform mints the connect URL server-side, per customer, and never hands out the bare hosted link.
Mailbox state
const mailbox = await aiinbx.mailboxes.retrieve("mbx_...")
console.log(mailbox.address, mailbox.state, mailbox.last_sync_at)mailbox = client.mailboxes.retrieve("mbx_...")
print(mailbox["address"], mailbox["state"])| State | Meaning | What to do |
|---|---|---|
active |
Syncing normally. | Nothing. |
needs_reauth |
The grant expired or was revoked — password change, admin policy, manual revocation. | Prompt the customer through the connect flow again. |
disconnected |
Removed, by you or by them. | Reconnect if they want it back. |
state_reason carries the provider’s explanation when there is one. Both transitions are also webhooks — mailbox.needs_reauth and mailbox.disconnected — which is the right place to trigger a re-authorization prompt rather than discovering it on the next failed send. The workspace’s owners and admins also get an email when a mailbox needs re-authorization, one per batch rather than per mailbox — each of them at the level they picked under Settings → Notifications.
Sending from a mailbox that isn’t active fails with 409 mailbox_inactive.
Syncing
Mail arrives on its own — Gmail through push notifications, Outlook through Graph subscriptions. sync forces a catch-up when you have a reason to think something was missed:
await aiinbx.mailboxes.sync("mbx_...") // 202, queuedclient.mailboxes.sync("mbx_...")curl -X POST https://api.aiinbx.com/api/v2/mailboxes/mbx_.../sync \
-H "Authorization: Bearer $AI_INBX_API_KEY"It returns 202 with { "status": "queued" } — the work happens in the background, and the messages surface as ordinary email.received events. It isn’t a way to poll for mail; it’s a repair tool.
Disconnecting
const mailbox = await aiinbx.mailboxes.disconnect("mbx_...")
console.log(mailbox.state) // "disconnected"
Sync stops and the stored credentials are dropped. Messages already received stay on their threads.
Your own OAuth app
By default the consent screen says AI Inbx. Register your own Google or Microsoft app and it says your product’s name instead — which is usually a requirement, not a preference, once you’re asking a business customer for mailbox access.
const app = await aiinbx.oauthApps.create({
provider: "google",
name: "Acme Assistant",
slug: "acme",
client_id: process.env.GOOGLE_CLIENT_ID!,
client_secret: process.env.GOOGLE_CLIENT_SECRET!,
tagline: "Let Acme handle your inbox",
support_email: "support@acme.com",
return_urls: ["https://app.acme.com/settings/mailboxes"],
})
console.log(app.redirect_uri) // register this with the provider
console.log(app.connect_url) // the hosted page; + "/start?return_to=…" is the connect link
console.log(app.push_endpoint) // Google only — the Pub/Sub push endpoint
provider"google" | "microsoft"
Which provider this app is registered with.
"google" | "microsoft"namestring
Shown to the customer during consent.
stringslugstring
3–64 characters, unique across AI Inbx; forms the hosted connect URL.
stringclient_idstring
From the provider's console.
stringclient_secretstring
Stored encrypted; never returned.
stringtenant?string
Microsoft only — restrict to a single Entra tenant.
stringpubsub_topic?string
Google only — your own Pub/Sub topic for push notifications.
stringaccent?string
Accent color for the hosted connect page.
stringtagline?string
One line shown under the app name.
stringsupport_email?string
Where a confused customer should write.
stringreturn_urls?string[]
Pages of yours a connect link may return to, up to 20. Matched on origin and path.
string[]Register the app with the provider
A Google Cloud OAuth client, or an Entra app registration.
Create it here and read back redirect_uri
Add that exact URI to the provider’s allowed redirect list.
Google only: create a Pub/Sub topic
Grant publish rights, then set pubsub_topic. Push notifications go to the
returned push_endpoint. A bring-your-own app needs its own topic — it
cannot share the shared app’s.
Connect through it
Share the hosted connect_url, put a connect link on your
site, or pass app_id to mailboxes.connect.
slug is claimed globally; a taken one fails with 409 slug_taken. A workspace configured to require its own app returns shared_apps_disabled if you try to connect without an app_id.
Secrets can be rotated with oauthApps.update; the old value is replaced immediately, so update the provider first.
Domains or mailboxes?
Not a question of whose address it is — either one can be yours or your customer’s. A domain you add on a customer’s behalf, walking them through the DNS records, is an ordinary domain. What differs is what you need from them and what you get.
| Domain | Mailbox | |
|---|---|---|
| Needs | Access to the domain’s DNS | One person clicking approve |
| Covers | Every address on the domain | One existing inbox |
| Mail is handled by | AI Inbx | The existing Google or Microsoft account, synced |
| Setup | Publish records, wait for DNS | OAuth consent, live in seconds |
| Receiving | The INBOUND MX record |
Provider sync |
| Breaks when | The records change or lapse | The grant is revoked |
The deciding question is usually whether the address already receives mail somewhere. An address on a live Google Workspace or Microsoft 365 domain has an inbox already, and pointing the MX record at AI Inbx would take that over for the whole domain — connect the mailbox instead. A domain with nowhere for its mail to go yet, or a subdomain created for the purpose, is the domain case.
Plenty of products use both, and often both on behalf of the same customer: a subdomain for anything the product sends, connected mailboxes for anything written from a person’s own inbox.