Invitations API
Invite people from your own server — when someone is hired, or a customer adds a user to their account — instead of an admin pasting addresses into the dashboard. It is the same as Inviting Members: the same rules, the same one email per person, the same Invitations tab to follow it in.
Before you start
- Under Settings → API keys, issue a key and tick Invitations. You need the Invite members permission, and the key can never do more than you can: the roles it may offer are the ones you may offer, and it stops working if you lose that permission or are deactivated.
- Store the key as a secret on your server. It is shown once.
Every request sends it as a bearer token:
Authorization: Bearer lmcp_live_...Invite people
POST /api/v1/invitations
curl -X POST "https://www.lockmcp.ai/api/v1/invitations" \
-H "Authorization: Bearer $LOCKMCP_API_KEY" \
-H "Idempotency-Key: 6f1c2a4e-9d3b-4c8a-b1f0-2e7d5a9c3b11" \
-H "Content-Type: application/json" \
-d '{
"defaults": {
"platformRole": "member",
"connections": [{ "connection": "acme", "roles": ["viewer"] }]
},
"roleMap": { "acme": { "Leasing Agent": ["leasing"] } },
"people": [
{ "email": "ana@acme.com", "name": "Ana", "sourceRole": "Leasing Agent" },
{ "email": "bo@acme.com", "connections": [{ "connection": "acme", "roles": ["editor"] }] }
]
}'| Field | Meaning |
|---|---|
people | 1–500 people: email, and optionally name, sourceRole (their role in your app), platformRole, connections |
defaults.platformRole | The workspace role everyone gets, e.g. member |
defaults.connections | The connections everyone is invited to, each with its roles |
roleMap | Per connection: your app's role names → hub roles. Matched to each person's sourceRole, ignoring case |
sendNow | true (default) sends the emails. false saves them as Not sent yet — see Send saved invitations |
dryRun | true checks everything and writes nothing — the answer shows what would happen, plus seats and today's allowance |
Connections and roles are referred to by slug (stable across environments) or id. A role belongs to one connection, so viewer in acme and viewer in ops are different roles.
Which roles a person gets
For each person and connection, the first of these that applies:
- The roles in the person's own
connectionsentry - The
roleMaproles for theirsourceRole - The roles in
defaults.connections - None given — the connection's default role, or the person is skipped if it has none
A person's own connections, when given, replaces the default list. Their platform role is their own platformRole, else defaults.platformRole. A sourceRole never sets the platform role, so a value of "admin" in your data can't make anyone an admin.
If any connection or role you name doesn't exist, the request is refused with the full list and nothing is written.
Idempotency
Send an Idempotency-Key header — any 8–200 characters of letters, digits and . _ : -, such as a UUID — and reuse it when you retry.
- The same key with the same body answers
200with the batch it already created and its current status (headerIdempotent-Replayed: true). Nobody is invited twice. - The same key with a different body is a
409 idempotency_key_reused. - It isn't needed for a
dryRun. A request that ends up inviting nobody writes nothing, so it records no key either.
The answer
202 Accepted — the invitations are saved and their emails queued:
{
"batchId": "c7f0…",
"sendNow": true,
"counts": { "invited": 2, "skipped": 0, "refused": 0 },
"people": [
{
"email": "ana@acme.com",
"status": "invited",
"existingMember": false,
"invitations": [
{ "id": "…", "connection": null, "roles": ["member"] },
{ "id": "…", "connection": "acme", "roles": ["leasing"] }
],
"skipped": []
}
]
}Each person is invited (with anything skipped for one connection and why), skipped (nothing to do — already a member there, already invited, a role they already hold elsewhere…) or refused (not a valid address, listed twice, a deactivated member).
| Status | Meaning |
|---|---|
202 | Saved; emails queued (or saved as not sent, with sendNow: false) |
200 | A dry run, a replay, or nobody to invite — nothing new written |
400 | Invalid body, unknown_reference, no_access_given, or idempotency_key_required |
401 | Missing, unknown, revoked or expired key, or its owner is deactivated |
403 | The key isn't for Invitations, or its owner lost Invite members |
402 | Not enough seats on your plan — nothing was saved; the body says how many are needed and free |
409 | idempotency_key_reused, or someone invited one of these people at the same moment — retry |
429 | Too many requests (honour Retry-After), or daily_invite_limit — see below |
Follow a batch
GET /api/v1/invitations/batches/{batchId}
Counts, then one row per invitation with its status — DRAFT, PENDING, ACCEPTED, DECLINED, REVOKED, EXPIRED — and the state of the email carrying it: PENDING, SENDING, SENT, or FAILED with the reason. Each person gets one email even when invited to several connections, so counts.emails counts emails, not invitations.
{
"id": "c7f0…",
"people": 2,
"counts": { "invitations": { "PENDING": 3 }, "emails": { "SENT": 2 } },
"invitations": [
{ "id": "…", "email": "ana@acme.com", "connection": "acme", "roles": ["leasing"], "status": "PENDING",
"delivery": { "status": "SENT", "attempts": 1, "sentAt": "2026-10-16T05:35:38Z" } }
]
}Emails usually go out within seconds. A failure the provider calls temporary is retried for several hours before it shows as FAILED.
Send saved invitations
POST /api/v1/invitations/send with { "batchId": "…" } or { "invitationIds": ["…"] } (up to 500).
Everything is checked again — a role may have been deleted, or you may have lost a permission, since they were saved — and seats and the daily allowance apply now. Anything that no longer qualifies is skipped with the reason. For a batch with more than 500, the answer carries a cursor: call again with { "batchId": "…", "after": "<cursor>" }. Sending twice sends once.
Daily invitation allowance
A workspace can send a limited number of invitation emails per rolling 24 hours: every new invitation, resend and sent draft counts, whether from the dashboard or this API. Past it, the request is refused whole with 429, "code": "daily_invite_limit", how many are left, and a Retry-After for when room frees up:
{ "code": "daily_invite_limit", "limit": 50, "used": 48, "needed": 5, "available": 2, "retryAt": "2026-10-17T05:35:38Z" }Split the list, or wait. If you regularly need more, contact support.
Where to go next
- Inviting Members — the same from the dashboard
- Roles and Permissions — what roles control
- Record Scope — the other thing an API key does