Engineering8 min read

Multi-tenant email with Programmable Inbox

How to provision per-customer inboxes, isolate data between tenants, and handle routing for SaaS applications.

SC

Sam Chen

Engineer · July 14, 2025

Multi-tenancy is a first-class concept in Programmable Inbox. This post explains how to structure your integration when you're building a SaaS product and need isolated email inboxes per customer.

Organizations and inboxes

The data model is:

Organization (your tenant)
  └── Inbox (one or more per org)
        └── Thread
              └── Message

Each organization has its own API key, and API calls are scoped to that organization. You can't accidentally read another org's messages.

Provisioning inboxes on signup

When a new customer signs up for your product, create an inbox for them:

ts
async function onCustomerSignup(customer: Customer) {
  const res = await fetch('https://api.programmableinbox.dev/orgs', {
    method: 'POST',
    headers: { Authorization: `Bearer ${MASTER_API_KEY}` },
    body: JSON.stringify({ name: customer.id }),
  })
  const org = await res.json()

  await fetch(`https://api.programmableinbox.dev/orgs/${org.id}/inboxes`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${MASTER_API_KEY}` },
    body: JSON.stringify({ name: 'support', domain: customer.domain }),
  })

  // Store org.apiKey in your database — scoped to this customer
  await db.customers.update({ id: customer.id, inboxApiKey: org.apiKey })
}

Scoped API keys

Once you've stored the per-org API key, use it for all inbox operations on behalf of that customer. It cannot access any other org's data, so you can safely expose it to your frontend if needed.

Webhook routing

If you want a single webhook endpoint to handle all organizations, include the org ID in the webhook URL:

bash
curl -X POST .../orgs/{orgId}/inboxes/{id}/webhooks \
  -d '{"url": "https://your-app.com/api/email?orgId={orgId}"}'

Or register a global webhook at the master API level that receives events from all orgs, with the org ID included in the payload.