Skip to content
AI Inbx
Esc
navigateopen⌘Jpreview
On this page

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_urlhttps://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
PropType
ref?string

Your own identifier for the person, up to 200 characters. Echoed on mailbox.connected.

Typestring
return_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.

Typestring

Brand the page from the app’s settings in the console, or through oauthApps.createaccent, tagline, support_email, logo_url.

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>
PropType
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.

Typestring
ref?string

Your own identifier for the person, up to 200 characters. Echoed on mailbox.connected.

Typestring

Register 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`.
PropType
provider"google" | "microsoft"

Which provider to authorize against.

Type"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.

Typestring
ref?string

Your own identifier, echoed back on mailbox.connected so you know which user finished.

Typestring
app_id?string

Use one of your own OAuth apps instead of the shared AI Inbx app — see below.

Typestring
space_id?string

The space the mailbox lands in, and with it everything synced from it. Omit for the workspace itself.

Typestring
region?"eu-central-1" | "us-east-1"

Where this mailbox's mail is processed and stored. Defaults to eu-central-1.

Type"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.

Typenumber

The 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, queued
client.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
PropType
provider"google" | "microsoft"

Which provider this app is registered with.

Type"google" | "microsoft"
namestring

Shown to the customer during consent.

Typestring
slugstring

3–64 characters, unique across AI Inbx; forms the hosted connect URL.

Typestring
client_idstring

From the provider's console.

Typestring
client_secretstring

Stored encrypted; never returned.

Typestring
tenant?string

Microsoft only — restrict to a single Entra tenant.

Typestring
pubsub_topic?string

Google only — your own Pub/Sub topic for push notifications.

Typestring
accent?string

Accent color for the hosted connect page.

Typestring
tagline?string

One line shown under the app name.

Typestring
support_email?string

Where a confused customer should write.

Typestring
return_urls?string[]

Pages of yours a connect link may return to, up to 20. Matched on origin and path.

Typestring[]

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.

Last updated on September 9, 2026

Was this page helpful?