Multi-tenant apps with pods
Isolate each of your customers in its own pod with its own inboxes, domains, webhooks, lists, events and pod-scoped API keys, using one MachineMail organization.
If you build a product where each of your customers gets agents with email, keep every customer in its own pod. A pod is a partition of your organization. A key scoped to a pod sees only that pod; everything outside it returns 404, as if it did not exist.
The pattern:
- One pod per end customer, created idempotently with your customer ID.
- One pod-scoped API key per customer, used by that customer’s agents.
- Inboxes, domains, webhooks and lists created inside the pod.
See Organizations and pods for the model.
Create a pod per customer
Use your own customer ID as
client_id. Repeating the call returns the existing pod with status 200, so it is safe in a provisioning job that may retry.Only organization-scoped keys can create pods. Store
pod_idnext to your customer record.Mint a pod-scoped key
Post to the pod’s
api-keysroute. The route decides the new key’s scope.api_keyis shown once; store it encrypted. Leave outpermissionsfor a key with full access inside the pod. Rules for minting:- Only organization-scoped keys can create keys. A pod key cannot mint more keys.
- A key with restricted permissions can only mint keys with a subset of its own permissions, never an unrestricted one.
expires_at(ISO 8601) makes the key stop working at that time.DELETE /v0/pods/{pod_id}/api-keys/{api_key_id}revokes it immediately.
See API keys for every permission.
Create the customer’s inboxes
Either use your organization key with
pod_id:or call with the pod key, which always creates inboxes in its own pod:
Inbox addresses are global. On the shared domain, two customers cannot both have
support@machinemail.dev, so prefix usernames with something customer-specific, or give each customer a custom domain (next step).Give the customer a domain (optional)
Add the customer’s domain with their pod key, or with the organization key on the pod route. The domain belongs to the pod, and catch-all inboxes on it are created in that pod.
Then follow Use a custom domain to publish and verify the records.
Pod-nested routes
Every resource route also exists under /v0/pods/{pod_id}. With an organization key, the prefix narrows the request to that pod, so your backend can act for one customer without holding their key:
| Route | Scope |
|---|---|
/v0/pods/{pod_id}/inboxes (and messages, threads, drafts under it) | The pod’s inboxes and mail |
/v0/pods/{pod_id}/threads, /v0/pods/{pod_id}/drafts | Across the pod’s inboxes |
/v0/pods/{pod_id}/webhooks | Webhooks created for the pod |
/v0/pods/{pod_id}/domains | The pod’s domains |
/v0/pods/{pod_id}/lists/{direction}/{type} | The pod’s allow and block lists |
/v0/pods/{pod_id}/metrics/events, /usage, /rates | The pod’s metrics |
/v0/pods/{pod_id}/events | The pod’s event log |
/v0/pods/{pod_id}/api-keys | The pod’s keys |
A pod key can use these routes for its own pod or the plain /v0/... routes; both give the same results. A pod key that names another pod gets 404.
Per-customer webhooks and events
You have two options for routing events to customers.
One webhook per pod. Create the webhook with the pod key, or on /v0/pods/{pod_id}/webhooks. It gets pod_id set and only fires for that pod’s events. Use this when each customer has their own endpoint.
One webhook for everything. Create one organization webhook and route by the pod_id or inbox_id in the payload. To limit it to some pods, pass pod_ids on create, and change the set later with add_pod_ids and remove_pod_ids on PATCH /v0/webhooks/{webhook_id}.
For streams, a pod key only ever streams its own pod. With an organization key, filter with ?pod_id= on GET /v0/events/stream or pod_ids in a WebSocket subscribe message. See Realtime events.
Per-customer lists
Allow and block lists exist at three levels, and all of them apply to an inbox: organization-wide, the inbox’s pod, and the inbox itself. Put a customer’s rules on their pod:
This switches the pod to allow-only: its inboxes now take mail only from acme.dev, and every other sender is labelled BLOCKED and withheld from agents. An allow-listed sender reaches verified only with an aligned DMARC pass. See Lists and suppressions.
Note
Suppressions (addresses that bounced or complained) are organization-wide, not per pod. A hard bounce from one customer’s send stops every pod from mailing that address until an organization-scoped key removes the suppression.
One agent, one inbox
To give a single agent the narrowest possible credential, mint an inbox-scoped key:
An inbox key reaches only that inbox. It cannot create inboxes or delete its own. This is a good fit for a key you hand to an LLM tool or put in a browser.
Offboarding a customer
Delete the pod’s inboxes (which purges their mail), domains and webhooks, revoke its keys, then delete the pod. Deleting a pod that still has inboxes returns 409 conflict:
Every organization has a default pod (is_default: true) that holds resources created without a pod. It cannot be deleted.