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

  1. 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.
  2. Store the key as a secret on your server. It is shown once.

Every request sends it as a bearer token:

bash
Authorization: Bearer lmcp_live_...

Invite people

POST /api/v1/invitations

bash
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"] }] }
    ]
  }'
FieldMeaning
people1–500 people: email, and optionally name, sourceRole (their role in your app), platformRole, connections
defaults.platformRoleThe workspace role everyone gets, e.g. member
defaults.connectionsThe connections everyone is invited to, each with its roles
roleMapPer connection: your app's role names → hub roles. Matched to each person's sourceRole, ignoring case
sendNowtrue (default) sends the emails. false saves them as Not sent yet — see Send saved invitations
dryRuntrue 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:

  1. The roles in the person's own connections entry
  2. The roleMap roles for their sourceRole
  3. The roles in defaults.connections
  4. 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 200 with the batch it already created and its current status (header Idempotent-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:

json
{
  "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).

StatusMeaning
202Saved; emails queued (or saved as not sent, with sendNow: false)
200A dry run, a replay, or nobody to invite — nothing new written
400Invalid body, unknown_reference, no_access_given, or idempotency_key_required
401Missing, unknown, revoked or expired key, or its owner is deactivated
403The key isn't for Invitations, or its owner lost Invite members
402Not enough seats on your plan — nothing was saved; the body says how many are needed and free
409idempotency_key_reused, or someone invited one of these people at the same moment — retry
429Too 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.

json
{
  "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:

json
{ "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