Team invitations
TryMellon tenants are the container for your team, your applications, and your billing. Inviting a teammate is the primary way new members join a tenant — and unlike most auth providers, invitations are tenant-scoped, role-aware, and acceptable without a pre-existing account.
This page covers the full lifecycle: issue, list, resend, revoke, accept.
Roles
| Role | Can |
|---|---|
owner | Everything. Manage billing, invite/remove owners, delete the tenant. Assigned at tenant creation — never via invitation. |
admin | Manage applications, members, webhooks, and policies. Invite other admins/developers/viewers. |
developer | Create and edit applications. No member or billing access. |
viewer | Read-only on all tenant resources. |
owner is special: it exists to guarantee that a tenant always has at least one person with full control. You cannot grant owner via invitation; ownership is promoted manually through support.
Lifecycle
┌──────────┐ POST /invitations ┌─────────┐
│ Admin │─────────────────────────>│ Pending │
└──────────┘ └─────────┘
│ │ │
token link (acceptLink) │ │ │
delivered by email │ │ │
│ │ │
POST /invitations/accept│ │ │
(invitee) ─────┘ │ │
ACCEPTED │ │
│ │
POST …/:id/resend (admin) ───────┘ │
new token, same invitation │
│
DELETE …/:id (admin) ─────┘
REVOKED
An invitation has exactly one terminal state: accepted, revoked, or expired. Once terminal it cannot transition further — re-inviting creates a new record.
Quick start
Invite a teammate
POST https://api.trymellonauth.com/v1/tenants/ten_01HXYZ…/invitations
Authorization: Bearer <session_token>
Content-Type: application/json
{
"email": "[email protected]",
"role": "developer"
}
Response — 201 Created:
{
"ok": true,
"data": {
"invitation_id": "inv_01HXYZ…",
"email": "[email protected]",
"role": "developer",
"expires_at": "2026-04-24T10:00:00.000Z",
"accept_link": "https://auth.trymellonauth.com/invite/accept?token=…"
}
}
TryMellon dispatches the invitation email automatically. accept_link is also returned so you can embed it in your own onboarding emails if you prefer first-party delivery.
Accept an invitation
The invitee doesn’t need a TryMellon account beforehand. When they open the link, your frontend (or the TryMellon-hosted fallback page) posts the token:
POST https://api.trymellonauth.com/v1/invitations/accept
Content-Type: application/json
{
"token": "itok_01HXYZ…"
}
Response — 200 OK:
{
"ok": true,
"data": {
"user_id": "user_…",
"tenant_id": "ten_…",
"role": "developer"
}
}
If the invitee is new, a user is created and the membership is granted atomically. If they already have an account, the membership is added to their existing user.
List pending invitations
GET https://api.trymellonauth.com/v1/tenants/ten_01HXYZ…/invitations?limit=50&include_expired=false
Authorization: Bearer <session_token>
Response:
{
"ok": true,
"data": {
"invitations": [
{
"invitation_id": "inv_…",
"email": "[email protected]",
"role": "developer",
"status": "pending",
"expires_at": "2026-04-24T10:00:00.000Z",
"created_at": "2026-04-17T10:00:00.000Z",
"resend_count": 0,
"last_resent_at": null
}
],
"next_cursor": null
}
}
Resend an invitation
If the first email was lost or ignored, resend issues a fresh token without creating a new invitation record. The audit trail preserves resend_count.
POST https://api.trymellonauth.com/v1/tenants/ten_…/invitations/inv_…/resend
Authorization: Bearer <session_token>
Response:
{
"ok": true,
"data": {
"invitation_id": "inv_…",
"accept_link": "https://auth.trymellonauth.com/invite/accept?token=…"
}
}
Revoke an invitation
DELETE https://api.trymellonauth.com/v1/tenants/ten_…/invitations/inv_…
Authorization: Bearer <session_token>
Response:
{
"ok": true,
"data": {
"invitation_id": "inv_…",
"status": "revoked"
}
}
Any further use of the previous token fails with INVITATION_REVOKED.
API reference
POST /v1/tenants/:tenant_id/invitations
Create an invitation. Requires an admin (or owner) session in that tenant.
-
Auth: Bearer session token.
-
Body:
Field Type Required Description emailstringyes Invitee email. role"admin" | "developer" | "viewer"yes Role to grant on acceptance. -
Returns:
{ invitation_id, email, role, expires_at, accept_link }.
GET /v1/tenants/:tenant_id/invitations
List invitations. Admin-only.
- Query:
limit(1–100, default 50)cursor(from previous response’snext_cursor)include_expired("true" | "false", defaultfalse)
POST /v1/tenants/:tenant_id/invitations/:invitation_id/resend
Reissue the token. Admin-only. Increments resend_count.
DELETE /v1/tenants/:tenant_id/invitations/:invitation_id
Revoke. Admin-only. Terminal.
POST /v1/invitations/accept
Public — no session required. The token is itself the secret.
- Body:
{ token: string }. - Returns:
{ user_id, tenant_id, role }.
Error codes
| HTTP | Code | Cause |
|---|---|---|
400 | invalid_email | Malformed email. |
400 | invalid_role | Role not in admin | developer | viewer. |
403 | forbidden | Caller is not an admin of the target tenant. |
404 | invitation_not_found | invitation_id unknown or scoped to another tenant. |
409 | invitation_already_accepted | Cannot resend or revoke an accepted invitation. |
410 | invitation_expired | Token past expires_at. Create a new one. |
410 | INVITATION_REVOKED | Token was revoked. |
409 | member_already_exists | The email is already a member of this tenant. |
Security model
- Tenant-scoped. An admin in tenant A cannot list, resend, or revoke invitations for tenant B. The ownership check fails before any use case runs.
- Token = secret. The
accept_linkgrants access on first use. Treat it like a password: don’t log it, don’t share it via insecure channels. - Single-use. An accepted token cannot be replayed.
- Bounded TTL. Invitations expire after 7 days by default. Configurable per tenant via dashboard.
- Rate limited. Admin endpoints are subject to the per-tenant rate limit (
rate-limit.plugin.ts); brute-forcing accept tokens is IP-rate-limited independently. - Audit trail. Every invitation event writes to the tenant audit log with the actor user ID, the target email, and the role.
Webhook events
| Event | When emitted |
|---|---|
invitation.issued | A new invitation was created. |
invitation.resent | An existing invitation was reissued. |
invitation.accepted | The invitee completed acceptance. |
invitation.revoked | An admin revoked the invitation before acceptance. |
invitation.expired | Invitation reached expires_at without being accepted (emitted asynchronously). |
See Webhook events for the envelope.