DocsOpen Moon AI
Browse the docs

Members

List and filter members, read a member’s record, suspend, reactivate or remove them, act in bulk, and export or import a CSV file with the admin API.

On this page

All routes live under https://chat.realmoon.ai/api/v1/orgs/{org_id}. Scopes: members:read to read, members:write to write. Invitations and access codes have their own page: Invitations and access codes.

A key never acts as the owner: it cannot promote, demote, suspend or remove an admin, and it never acts on its own creator (403 forbidden).

The member objectLink to the The member object section

JSON
{
  "user_id": "5b1e7c2a-3d4f-4e6a-8b9c-0d1e2f3a4b5c",
  "role": "member",
  "status": "active",
  "joined_at": 1767225600,
  "name": "Camille Durand",
  "email": "camille.durand@ecole.fr",
  "profile_image_url": "/user.png",
  "last_active_at": 1798200000,
  "never_active": false,
  "groups": [
    { "id": "2d4c6e8a-1b3d-4f5a-9c7e-0a2b4c6d8e1f", "name": "2nde 3", "kind": "static", "position": 0, "is_manager": false, "source": "import", "via": "manual" }
  ],
  "labels": [{ "id": "9b8a7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "key": "promo", "value": "2026", "color": "blue", "kind": "member" }],
  "roles": [],
  "role_sources": [],
  "effective_roles": [],
  "license": { "pool_id": "4e5f6a7b-8c9d-4e0f-a1b2-c3d4e5f6a7b8", "plan_id": "<plan id>", "plan_name": "<plan name>", "source": "rule" }
}
FieldDescription
roleBase role: owner, admin or member.
statusactive, suspended or pending.
emailnull when the key cannot see email addresses (its creator only reads the members of the groups they manage).
never_activeThe account has never been used.
groupsThe member’s groups; via is manual (added by hand) or label (matched by the rule of a dynamic group).
rolesRoles given directly; role_sources lists each role with its origin (direct, group or label); effective_roles is their union.
licenseThe member’s license (pool_id, plan_id, plan_name, source), or null. Only included in the list.

List membersLink to the List members section

GET /members · members:read

ParameterTypeDescription
qstringSearches the name and, if the key can see email addresses, the email. 128 characters.
group_idstringMembers of a group (added by hand or matched by its rule).
label_idstringMembers who carry a label.
rolestringowner, admin or member (base role), or manager, billing, custom:<id> (a role received directly, through a group or through a label).
statusstringactive, suspended or pending.
no_groupbooleantrue: members of no group.
never_activebooleantrue: accounts never used.
licensestringThe ID of a license pool, or none (no license).
sortstringname (default), last_active_at or joined_at.
dirstringasc or desc; defaults to asc for the name, desc otherwise.
page, limitintegerPagination.

The response also includes emails_visible, license_pools (the pools: id, plan_id, plan_name, name, seats, used; empty for a single-license workspace), licenses_available, sort and dir. An unknown filter value returns 400 invalid_request; license on an organization without pools returns 400 licenses_unavailable.

cURL
curl "https://chat.realmoon.ai/api/v1/orgs/$ORG_ID/members?status=active&never_active=true&limit=200" \
  -H "Authorization: Bearer $MOONAI_ADMIN_KEY"
Python
import os
import time
import requests

BASE = f"https://chat.realmoon.ai/api/v1/orgs/{os.environ['ORG_ID']}"
HEADERS = {"Authorization": f"Bearer {os.environ['MOONAI_ADMIN_KEY']}"}

def all_members(**filters):
    page = 1
    while True:
        r = requests.get(f"{BASE}/members", headers=HEADERS,
                         params={**filters, "page": page, "limit": 200}, timeout=30)
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "5")))
            continue
        r.raise_for_status()
        body = r.json()
        yield from body["items"]
        if page * 200 >= body["total"]:
            break
        page += 1

for m in all_members(status="active", never_active="true"):
    print(m["user_id"], m["email"])
JavaScript
const BASE = `https://chat.realmoon.ai/api/v1/orgs/${process.env.ORG_ID}`;
const HEADERS = { Authorization: `Bearer ${process.env.MOONAI_ADMIN_KEY}` };

async function* allMembers(filters = {}) {
  for (let page = 1; ; page++) {
    const params = new URLSearchParams({ ...filters, page: String(page), limit: '200' });
    const r = await fetch(`${BASE}/members?${params}`, { headers: HEADERS });
    if (!r.ok) throw new Error(await r.text());
    const body = await r.json();
    yield* body.items;
    if (page * 200 >= body.total) break;
  }
}

for await (const m of allMembers({ status: 'active', never_active: 'true' })) console.log(m.user_id, m.email);

Read a member’s recordLink to the Read a member’s record section

GET /members/{user_id} · members:read

JSON
{
  "member": {
    "user_id": "5b1e7c2a-3d4f-4e6a-8b9c-0d1e2f3a4b5c",
    "name": "Camille Durand",
    "email": "camille.durand@ecole.fr",
    "role": "member",
    "status": "active",
    "joined_at": 1767225600,
    "last_active_at": 1798200000,
    "never_active": false,
    "invited_by": { "user_id": "a11f2b3c-4d5e-4f6a-8b7c-9d0e1f2a3b4c", "name": "Fabien Marie" }
  },
  "base_role_name": null,
  "roles": [{ "key": "manager", "via": "group", "via_id": "7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f", "via_name": "Professeurs", "name": "Enseignant", "removable": false }],
  "groups": [],
  "labels": [],
  "license": null,
  "license_pools": [],
  "usage": { "files": 14, "monthly_percent": 12.5, "week_percent": 30.0, "session_percent": 0.0, "period_end": 1800000000, "tier": "paid" },
  "activity": null,
  "emails_visible": true,
  "blocked": null,
  "can": { "suspend": true, "remove": true, "base_role": false, "admin_role": false, "groups": false, "labels": false, "roles": false, "license": false, "rules": false, "usage": false, "activity": false }
}
FieldDescription
usageThe member’s usage as a percentage of their limits (month, week, session) and their number of files; null without the right to read usage.
activityThe last 15 audit log actions that concern them; null without the right to read the log.
blockedWhy the key cannot act on this member: self, owner, admin or stronger (they have more rights).
canWhat the key can do to this member.

Suspend, reactivate or change the base roleLink to the Suspend, reactivate or change the base role section

PATCH /members/{user_id} · members:write

FieldTypeRequiredDescription
statusstringnosuspended (for an active member) or active (for a suspended member).
rolestringnoadmin or member. Owner only: refused to a key.

Send at least one field. Response: {"member": {…}}, the member after the change.

cURL
curl -X PATCH https://chat.realmoon.ai/api/v1/orgs/$ORG_ID/members/$USER_ID \
  -H "Authorization: Bearer $MOONAI_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "suspended"}'
Python
r = requests.patch(f"{BASE}/members/{user_id}", headers=HEADERS, json={"status": "suspended"}, timeout=30)
r.raise_for_status()
print(r.json()["member"]["status"])
JavaScript
const r = await fetch(`${BASE}/members/${userId}`, {
  method: 'PATCH',
  headers: { ...HEADERS, 'Content-Type': 'application/json' },
  body: JSON.stringify({ status: 'suspended' }),
});
if (!r.ok) throw new Error(await r.text());
console.log((await r.json()).member.status);

Suspending a member signs them out of all their sessions and sends the member.suspended event. Reactivating a member takes a seat: on a workspace paid by card, the seat is paid first; the member.reactivated event is sent afterward.

StatuscodeCause
400invalid_requestNothing to change; suspending a member who is not active; reactivating a member who is not suspended.
402seat_payment_failedThe seat payment failed: the owner must update their payment method.
403forbiddenThe key’s own creator, the owner, an admin, or a member with more rights than the key.
404not_foundNot a member of the organization.
409seats_fullNo free seat left.
409past_dueThe workspace’s last payment failed.
409payment_method_requiredThe new owner must add a payment method.

Remove a memberLink to the Remove a member section

DELETE /members/{user_id} · members:write

Removes the member from the organization; their Moon AI account remains. Their sessions in the organization are closed, and their groups, roles, labels and organization connections are deleted, along with the invitations, access codes and keys they created. The member.removed event is sent. Response: {"removed": true}. Same refusals as PATCH, plus 409 if the organization would be left without an active admin.

To hand over their resources first, use offboarding a member.

Act on several membersLink to the Act on several members section

POST /members/bulk · members:write

FieldTypeRequiredDescription
actionstringyesSee below.
user_idslistuser_ids or filterUp to 5,000 members. IDs that are not members are ignored.
filterobjectuser_ids or filterThe list filters: q, group_id, label_id, role, status (active or suspended), no_group, never_active, license. Up to 20,000 members. Not allowed with remove.
valuestringdepends on the actionThe target group, label or role.
is_managerbooleannoWith add_group: make them managers.
pool_idstringwith licenseA pool, "none" (no license) or null (back to the rules).
actionvalueEffect
suspend, reactivatenoneSuspend, reactivate. On a workspace paid by card, up to 50 reactivations per request.
set_base_roleadmin or memberOwner only.
add_group, remove_groupgroup IDAdd to the group, remove from it.
add_label, remove_labellabel IDPut on or take off a member label.
add_role, remove_rolemanager, billing or custom:<id>Give or remove a role (also requires the roles:write scope).
licensenone (pool_id)Assign a license pool (also requires the licenses:write scope).
removenoneRemove from the organization.
JSON
{
  "action": "suspend",
  "matched": 42,
  "done": 39,
  "skipped": { "admin": 1, "already": 2 },
  "failed": [],
  "failed_count": 0
}

skipped counts the members left out, by reason: self, owner, admin, stronger, already, not_member, out_of_scope, not_active. failed gives the first 50 failures (user_id, code, message). There is no overall transaction: what is done stays done if a later member fails.

Export membersLink to the Export members section

GET /members/export · members:read

Accepts the list filters (except pagination and sorting) and lang=en for English column headers. Returns a CSV file (UTF-8 with BOM, ; separator), sorted by name, up to 50,000 lines:

Text
email;nom;groupes;étiquettes;rôle de base;rôles;licence;statut;arrivé le;dernière activité
camille.durand@ecole.fr;Camille Durand;2nde 3;promo=2026;member;;<plan>;active;2026-01-01 00:00;2026-12-23 08:40

Multiple values are separated by |, and dates are in UTC. A cell that starts with =, +, - or @ is prefixed with an apostrophe.

Import a CSV fileLink to the Import a CSV file section

POST /members/import · members:write

Imports up to 5,000 lines (2 MB): each line invites a person, adds them directly (email address on a verified domain, organization not paid by card) or updates a member (groups, labels, role and license are added; nothing is ever removed).

ParameterTypeDescription
dry_runbooleantrue: simulation, nothing is written.
create_missingbooleantrue: creates unknown groups and labels.

You can send the file in three ways: multipart/form-data with a file field, application/json with {"csv": "<text>"}, or the raw CSV (text/csv).

ColumnContent
emailRequired.
nameDisplay name.
groupsGroup names, separated by | or ;.
labelskey=value pairs, separated by | or ;.
rolemember, admin (owner only), manager, billing, or the key of a custom role.
licenseThe ID or plan of a pool.

The header row is recognized in French or English (GET /members/import/columns returns the accepted names); without a header row, the columns are read in this order. The separator (,, ; or tab) is detected automatically.

cURL
curl "https://chat.realmoon.ai/api/v1/orgs/$ORG_ID/members/import?dry_run=true" \
  -H "Authorization: Bearer $MOONAI_ADMIN_KEY" \
  -F "file=@eleves.csv"
Python
with open("eleves.csv", "rb") as f:
    r = requests.post(f"{BASE}/members/import", headers=HEADERS,
                      params={"dry_run": "true"}, files={"file": f}, timeout=120)
r.raise_for_status()
report = r.json()
print(report["summary"])
for line in report["lines"]:
    if line["errors"]:
        print(line["line"], line["email"], line["errors"])
JavaScript
import fs from 'node:fs';

const form = new FormData();
form.append('file', new Blob([fs.readFileSync('eleves.csv')], { type: 'text/csv' }), 'eleves.csv');
const r = await fetch(`${BASE}/members/import?dry_run=true`, { method: 'POST', headers: HEADERS, body: form });
if (!r.ok) throw new Error(await r.text());
const report = await r.json();
console.log(report.summary);

The response gives summary (lines per action, errors, seats left), to_create (groups and labels to create) and lines: for each line, action (invite, add_domain, update, already_member, already_invited or error), errors and warnings. An error on one line does not stop the import: the other lines are processed.

Outside a simulation, an import of up to 100 lines responds right away with the result and a job_id. Above that, it responds 202 with {"job_id", "status": "running", "total"} and continues in the background. Follow it with GET /members/import/{job_id}, which returns status (running, done, failed or interrupted), total, done, phase and, at the end, result (the lines with errors or warnings, up to 2,000). A job’s progress is kept for 24 hours.

Each invitation sent by the import counts toward the invitation limits; the simulation does not check them.

See alsoLink to the See also section

Moon AI

Moon AI in action

0:00

Explore

Screenshots of chat.realmoon.ai taken on October 29, 2026, with fictional data.

Moon AI

Your preferences

Visits counted without cookies.

Our server counts visits without cookies or trackers. Your theme and your answer about ad measurement are remembered on this device.

Ad measurement

With your consent, our ad networks’ trackers (Google) measure our ads, from the visit to the sign-up and then to the first subscription and its amount, and may show you our ads. Your answer is valid for 13 months; you can change it here at any time.

No answer saved on this device.

After you decline, no tag is loaded anymore and the site deletes the cookies they had set on realmoon.ai. Those on the ad networks’ own domains are managed with them or in your browser.

The Moon AI app has its own privacy settings.