Core concepts
Sub-projects (multi-tenant SaaS pattern)
If you're building a SaaS app that sends transactional email on behalf of your own end-customers, sub-projects let you partition sends, traces, and quotas per downstream tenant. Your app authenticates once (as the master project), every send carries the tenant's identity, and the dashboard / webhook payloads stay per-tenant.
When to use what
Pick the shape that matches your tenancy model:
| Recipe | Use when | Notes |
|---|---|---|
Sub-projects + customer_id | The standard B2B SaaS shape. Your app authenticates as the master, partitions traces and destinations per downstream tenant, and stamps every send with the tenant's id. | Single mailnix billing relationship (yours). Per-tenant send-cap overrides. Recommended for most SaaS integrations. |
customer_id only (no sub-projects) | Per-tenant attribution but every customer shares the same destination + quota pool. | Cheapest. Can't isolate one tenant's bad reputation from another's. Useful for low-volume tenants on a uniform plan. |
| One mailnix organization per customer | You're a payments / fintech app where every customer-of-customer is a distinct billing relationship with audit-isolation requirements (HIPAA, KYC, etc.). | One Stripe customer per downstream customer. Operationally heavy. |
The rest of this guide walks the recommended sub-projects + customer_id recipe.
The data model
organization (your mailnix billing tenant)
│
├── master_project (your app authenticates as this)
│ │
│ ├── sub_project: customer-alpha
│ ├── sub_project: customer-beta
│ └── sub_project: customer-gamma
│
└── destination(s) (SMTP / SES / Mailgun / Postmark / …)- The master project is what your app's backend authenticates as (one OAuth client, one set of API keys).
- Each sub-project has its own id, dev key, and optional send-cap override. Sub-projects inherit
organization_idfrom the master. - The
customer_idattribute on a send is a separate axis from sub-projects: you can passcustomer_ideven when sending through a sub-project for clearer downstream attribution.
The hierarchy caps at one level deep: a sub-project cannot itself be a parent.
End-to-end integration (REST)
The mailnix REST API is the integration surface today; there is no official mailnix client library yet. Call it from any HTTP client in your language of choice. The shape below is the canonical recipe.
# 1. Mint a sub-project for a new tenant.
curl -X POST https://api.mailnix.ch/v1/projects/sub \
-H "Authorization: Bearer $MAILNIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"parent_project_id": "proj_…master…",
"name": "tenant:acme-corp",
"send_cap_override": 50000
}'
# 2. Send a transactional message with per-customer attribution.
# customer_id is per-tenant attribution; trace lookups + webhook
# payloads carry it; max 128 chars (over-length is truncated).
curl -X POST https://api.mailnix.ch/v1/messages \
-H "Authorization: Bearer $MAILNIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": ["user@acme-corp.example"],
"from": "noreply@my-saas.example",
"subject": "Welcome",
"html_body": "<p>Hello.</p>",
"customer_id": "acme-corp"
}'
# 3. Filter traces to one downstream tenant.
curl "https://api.mailnix.ch/v1/traces?customer_id=acme-corp&limit=50" \
-H "Authorization: Bearer $MAILNIX_API_KEY"The full request/response shapes live in the OpenAPI spec. For AI-agent-driven integrations the same surface is exposed via MCP at mcp.mailnix.ch.
Routing webhooks by tenant
When you subscribe an outbound webhook to a master project (or a sub-project), every event payload includes the customer_id you stamped at send time. A customer-side router looks like this:
app.post("/webhooks/mailnix", verifySignature, (req, res) => {
const event = req.body;
// event.customer_id is the value you set on the send.
const handler = tenantHandlers[event.customer_id] ?? defaultHandler;
handler(event);
res.status(204).end();
});Per-tenant send-cap overrides
By default, every sub-project shares the master project's tier quota. To budget a specific tenant differently (e.g. lift a VIP above the default, or cap an abusive tenant), set the override on the sub-project:
curl -X PATCH https://api.mailnix.ch/v1/projects/sub/{sub_id}/quota \
-H "Authorization: Bearer $MAILNIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"send_cap_override": 100000}'Set send_cap_override to null to clear the override and fall back to the org tier default.
Delete cascade
Deleting a sub-project cascades to every trace, destination, and inbound route under it. The master project is not affected. Deleting the master project cascades to every sub-project as well as the master's own children. Both are irreversible; the dashboard prompts for typed confirmation.
Tier requirement
Sub-projects require the Team tier or higher. The dashboard shows an upgrade prompt for callers below tier; the API returns HTTP 402 with the structured upgrade_url payload.