All routes live under https:// and require the members:write scope, including for reads. An invitation is valid for 7 days; the person who accepts it joins the organization with everything that was set up for them, and the member.joined webhook event is sent at that point.
A key never grants the admin role: an admin invitation or access code is reserved for the owner of the organization (403 ).
The invitation objectLink to the The invitation object section
{
"id": "7e2a4c6b-8d0f-4a1b-9c3d-5e7f9a1b3c5d",
"email": "tom.bernard@ecole.fr",
"role": "member",
"created_by": "a11f3c5e-7b9d-4f1a-8c2e-4d6f8a0b2c4e",
"created_by_name": "Camille Martin",
"created_at": 1762339200,
"expires_at": 1762944000,
"expired": false,
"groups": [{ "id": "<group id>", "name": "Seconde 3" }],
"labels": [],
"roles": [{ "key": "manager", "name": "Responsable" }],
"license": null
}groups, labels, roles and license are what the person will receive when they join. When an invitation is resent, its author becomes the person or key that resent it.
Read the limitsLink to the Read the limits section
GET / · members:write
{
"under_contract": false,
"paid": true,
"seats_left": 12,
"pending": 4,
"pending_cap": 200,
"daily": { "workspace": 50, "sender": 50, "recipient": 3 },
"managed": false,
"email_configured": true,
"max_emails": 200
}| Field | Description |
|---|---|
seats_left | Free seats minus pending invitations; null when there is no cap. |
pending, pending_cap | Pending invitations and their cap; null under contract. |
daily | Invitations per day: for the workspace, for one sender and for one email address. null under contract. |
email_configured | Email sending is set up; if not, share each invitation link yourself. |
max_emails | Maximum number of email addresses per request. |
Daily limits count over a rolling 24 hours. For a key, the sender is the person who created it. The same email address receives at most 3 invitations per day from one organization, even under contract.
InviteLink to the Invite section
POST / · members:write
| Field | Type | Required | Description |
|---|---|---|---|
emails | list | yes | 1 to 200 email addresses. |
base_role | string | no | member (default) or admin (owner only). |
role_keys | list | no | Up to 20 roles, given on joining; requires roles:write. |
group_ids | list | no | Up to 50 static groups; requires groups:write. |
label_ids | list | no | Up to 50 member labels; requires labels:write. |
pool_id | string | no | The person’s license pool; requires licenses:write. |
The same roles, groups, labels and pool apply to every email address in the request.
curl https://chat.realmoon.ai/api/v1/orgs/$ORG_ID/invitations \
-H "Authorization: Bearer $MOONAI_ADMIN_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: rentree-2026-seconde-3" \
-d '{"emails": ["tom.bernard@ecole.fr", "lea.dupont@ecole.fr"], "group_ids": ["<group id>"]}'import os
import requests
BASE = f"https://chat.realmoon.ai/api/v1/orgs/{os.environ['ORG_ID']}"
HEADERS = {
"Authorization": f"Bearer {os.environ['MOONAI_ADMIN_KEY']}",
"Idempotency-Key": "rentree-2026-seconde-3",
}
r = requests.post(
f"{BASE}/invitations",
headers=HEADERS,
json={"emails": ["tom.bernard@ecole.fr", "lea.dupont@ecole.fr"], "group_ids": [os.environ["GROUP_ID"]]},
timeout=60,
)
r.raise_for_status()
for line in r.json()["results"]:
print(line["email"], line["status"], line["code"])const BASE = `https://chat.realmoon.ai/api/v1/orgs/${process.env.ORG_ID}`;
const r = await fetch(`${BASE}/invitations`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MOONAI_ADMIN_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'rentree-2026-seconde-3',
},
body: JSON.stringify({ emails: ['tom.bernard@ecole.fr', 'lea.dupont@ecole.fr'], group_ids: [process.env.GROUP_ID] }),
});
if (!r.ok) throw new Error(await r.text());
for (const line of (await r.json()).results) console.log(line.email, line.status, line.code);{
"results": [
{ "email": "tom.bernard@ecole.fr", "status": "invited", "code": null, "message": null, "invitation_id": "7e2a4c6b-8d0f-4a1b-9c3d-5e7f9a1b3c5d", "expires_at": 1762944000, "link": "https://chat.realmoon.ai/invite/<token>" },
{ "email": "lea.dupont@ecole.fr", "status": "already_member", "code": null, "message": null, "invitation_id": null }
],
"summary": { "invited": 1, "reinvited": 0, "already_member": 1, "errors": 0 },
"email_sent": true
}Each email address gets its own line:
status | Meaning |
|---|---|
invited | Invitation created. |
reinvited | A pending invitation already existed: it gets a new link (the old one stops working) and 7 more days. It does not take an extra seat. |
already_member | The person is already a member, active or suspended. |
error | Nothing was created for this email address; code and message say why. |
code | Cause |
|---|---|
invalid_email | Invalid email address. |
duplicate | Email address listed twice in the request. |
seats_full | Every seat is taken or held by a pending invitation. |
invite_pending_limit | Too many pending invitations: revoke some or wait until they are accepted. |
invite_limit_workspace, invite_limit_sender, invite_limit_recipient | Daily limit reached for the workspace, the sender or the email address. |
error | The invitation could not be created. |
link is the person’s invitation link: treat it as a secret. email_sent says that email sending is set up, not that the email was received.
Errors for the whole request:
| Status | code | Cause |
|---|---|---|
| 400 | invalid_request | admin, owner or member role in role_keys; unknown role or label; a group that is dynamic, comes from the directory or grants the admin role; unknown license pool. |
| 400 | licenses_unavailable | pool_id while the organization has no licenses. |
| 403 | owner_only_admin_role | base_role: . |
| 403 | role_escalation | A role, group or label grants more than the key’s permissions. |
| 403 | license_change_forbidden | The groups, labels or roles grant a different license, with no pool_id and no right over licenses. |
List invitationsLink to the List invitations section
GET / · members:write
| Parameter | Description |
|---|---|
q | Part of the email address. |
status | pending (default) or expired (expired within the last 30 days). |
page, limit | Pagination. |
Most recent first. Accepted or revoked invitations are not listed.
Resend an invitationLink to the Resend an invitation section
POST / · members:write
Creates a new link (the old one is disabled), valid for 7 days, with the same groups, roles, labels and license. An expired invitation can be resent if a seat is still free.
{ "invitation_id": "7e2a4c6b-8d0f-4a1b-9c3d-5e7f9a1b3c5d", "expires_at": 1762944000, "link": "https://chat.realmoon.ai/invite/<token>", "email_sent": true }| Status | code | Cause |
|---|---|---|
| 404 | not_found | Unknown, accepted or revoked invitation. |
| 409 | seats_full | Expired invitation and no free seat left. |
| 429 | invite_limit_recipient | Too many invitations sent to this email address today. |
Revoke an invitationLink to the Revoke an invitation section
DELETE / · members:write
Response: {"success": . The link is disabled and everything set up for the person is deleted. Revoking it a second time returns 404.
Access codesLink to the Access codes section
An access code lets people join the organization without a personal invitation: 8 characters drawn from uppercase letters and digits, without I, O, 0 or 1.
GET / · members:write
{ "items": [{ "id": "c3b2a1f0-9e8d-4c7b-a6f5-e4d3c2b1a0f9", "code": "K7MZ4QPA", "role": "member", "max_uses": 30, "uses_count": 12, "expires_at": 1762944000, "created_at": 1762339200 }] }Only usable codes (not revoked, not expired, not used up) are listed, most recent first.
POST / · members:write
| Field | Type | Required | Description |
|---|---|---|---|
role | string | no | member (default) or admin (owner only). |
max_uses | integer | no | From 1 to 1,000, or -1 for no limit; 1 by default. |
expires_in_days | integer | no | From 1 to 365; 7 by default. |
An empty body {} creates a single-use code valid for 7 days. The response is the new code. A code is revoked automatically when its creator leaves the organization or loses the admin role.
DELETE / · members:write
Response: {"success": , also for a code that is already revoked or expired.