Toutes les routes sont sous https://. Portées : members:read pour lire, members:write pour écrire. Les invitations et les codes d’accès ont leur page : Invitations et codes d’accès.
Une clé n’agit jamais comme le propriétaire : elle ne nomme, ne rétrograde, ne suspend ni ne retire un administrateur, et n’agit jamais sur son propre créateur (403 ).
L’objet membreLien vers la section L’objet membre
{
"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": "<id du forfait>", "plan_name": "<nom du forfait>", "source": "rule" }
}| Champ | Description |
|---|---|
role | Rôle de base : owner, admin ou member. |
status | active, suspended ou pending. |
email | null quand la clé ne voit pas les adresses (son créateur ne lit que les membres des groupes qu’il gère). |
never_active | Le compte n’a jamais été utilisé. |
groups | Groupes du membre ; via vaut manual (ajouté) ou label (retenu par la règle d’un groupe dynamique). |
roles | Rôles attribués directement ; role_sources donne chaque rôle et son origine (direct, group ou label) ; effective_roles leur union. |
license | Licence du membre (pool_id, plan_id, plan_name, source), ou null. Présente dans la liste seulement. |
Lister les membresLien vers la section Lister les membres
GET /members · members:read
| Paramètre | Type | Description |
|---|---|---|
q | chaîne | Recherche dans le nom et, si la clé voit les adresses, l’adresse. 128 caractères. |
group_id | chaîne | Membres d’un groupe (ajoutés ou retenus par sa règle). |
label_id | chaîne | Membres qui portent une étiquette. |
role | chaîne | owner, admin ou member (rôle de base), ou manager, billing, custom:<id> (rôle reçu directement, par un groupe ou une étiquette). |
status | chaîne | active, suspended ou pending. |
no_group | booléen | true : membres d’aucun groupe. |
never_active | booléen | true : comptes jamais utilisés. |
license | chaîne | Identifiant d’un lot de licences, ou none (sans licence). |
sort | chaîne | name (par défaut), last_active_at ou joined_at. |
dir | chaîne | asc ou desc ; par défaut asc pour le nom, desc sinon. |
page, limit | entier | Pagination. |
La réponse ajoute emails_visible, license_pools (les lots : id, plan_id, plan_name, name, seats, used ; vide pour un workspace à licence unique), licenses_available, sort et dir. Une valeur de filtre inconnue répond 400 ; license sur une organisation sans lots répond 400 .
curl "https://chat.realmoon.ai/api/v1/orgs/$ORG_ID/members?status=active&never_active=true&limit=200" \
-H "Authorization: Bearer $MOONAI_ADMIN_KEY"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"])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);Lire la fiche d’un membreLien vers la section Lire la fiche d’un membre
GET / · members:read
{
"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 }
}| Champ | Description |
|---|---|
usage | Utilisation du membre en pourcentage de ses limites (mois, semaine, session) et nombre de fichiers ; null sans le droit de lire l’utilisation. |
activity | Les 15 dernières actions du journal qui le visent ; null sans le droit de lire le journal. |
blocked | Pourquoi la clé ne peut pas agir sur ce membre : self, owner, admin ou stronger (il a plus de droits). |
can | Ce que la clé peut faire sur ce membre. |
Suspendre, réactiver ou changer le rôle de baseLien vers la section Suspendre, réactiver ou changer le rôle de base
PATCH / · members:write
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
status | chaîne | non | suspended (membre actif) ou active (membre suspendu). |
role | chaîne | non | admin ou member. Réservé au propriétaire : refusé à une clé. |
Envoyez au moins un champ. Réponse : {"member": , le membre après le changement.
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"}'r = requests.patch(f"{BASE}/members/{user_id}", headers=HEADERS, json={"status": "suspended"}, timeout=30)
r.raise_for_status()
print(r.json()["member"]["status"])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);Suspendre ferme toutes les sessions du membre et envoie l’événement member.suspended. Réactiver occupe un siège : sur un workspace payé par carte, le siège est payé d’abord ; l’événement member.reactivated part ensuite.
| Statut | code | Cause |
|---|---|---|
| 400 | invalid_request | Rien à modifier ; suspendre un membre qui n’est pas actif ; réactiver un membre qui n’est pas suspendu. |
| 402 | seat_payment_failed | Le paiement du siège a échoué : le propriétaire doit mettre à jour son moyen de paiement. |
| 403 | forbidden | Le créateur de la clé lui-même, le propriétaire, un administrateur, ou un membre qui a plus de droits que la clé. |
| 404 | not_found | Ce n’est pas un membre de l’organisation. |
| 409 | seats_full | Plus de siège libre. |
| 409 | past_due | Le dernier paiement du workspace a échoué. |
| 409 | payment_method_required | Le nouveau propriétaire doit ajouter un moyen de paiement. |
Retirer un membreLien vers la section Retirer un membre
DELETE / · members:write
Retire le membre de l’organisation ; son compte Moon AI reste. Ses sessions dans l’organisation sont fermées, ses groupes, rôles, étiquettes et connexions de l’organisation sont supprimés, ainsi que les invitations, codes d’accès et clés qu’il avait créés. L’événement member.removed part. Réponse : {"removed": . Mêmes refus que PATCH, plus 409 si l’organisation n’aurait plus d’administrateur actif.
Pour transférer d’abord ses ressources, utilisez le départ d’un membre.
Agir sur plusieurs membresLien vers la section Agir sur plusieurs membres
POST / · members:write
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
action | chaîne | oui | Voir ci-dessous. |
user_ids | liste | user_ids ou filter | 5 000 membres au plus. Les identifiants qui ne sont pas des membres sont ignorés. |
filter | objet | user_ids ou filter | Les filtres de la liste : q, group_id, label_id, role, status (active ou suspended), no_group, never_active, license. 20 000 membres au plus. Interdit avec remove. |
value | chaîne | selon l’action | Groupe, étiquette ou rôle visé. |
is_manager | booléen | non | Avec add_group : nommer gestionnaires. |
pool_id | chaîne | avec license | Un lot, "none" (sans licence) ou null (retour aux règles). |
action | value | Effet |
|---|---|---|
suspend, reactivate | aucune | Suspendre, réactiver. Sur un workspace payé par carte, 50 réactivations au plus par requête. |
set_base_role | admin ou member | Réservé au propriétaire. |
add_group, remove_group | identifiant de groupe | Ajouter au groupe, en retirer. |
add_label, remove_label | identifiant d’étiquette | Poser, retirer une étiquette de membre. |
add_role, remove_role | manager, billing ou custom:<id> | Attribuer, retirer un rôle (portée roles:write en plus). |
license | aucune (pool_id) | Attribuer un lot de licences (portée licenses:write en plus). |
remove | aucune | Retirer de l’organisation. |
{
"action": "suspend",
"matched": 42,
"done": 39,
"skipped": { "admin": 1, "already": 2 },
"failed": [],
"failed_count": 0
}skipped compte les membres laissés de côté par raison : self, owner, admin, stronger, already, not_member, out_of_scope, not_active. failed donne les 50 premiers échecs (user_id, code, message). Il n’y a pas de transaction globale : ce qui est fait reste fait si un membre suivant échoue.
Exporter les membresLien vers la section Exporter les membres
GET / · members:read
Accepte les filtres de la liste (sauf la pagination et le tri) et lang=en pour des en-têtes en anglais. Rend un fichier CSV (UTF-8 avec BOM, séparateur ;), trié par nom, 50 000 lignes au plus :
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;;<forfait>;active;2026-01-01 00:00;2026-12-23 08:40Les valeurs multiples sont séparées par |, les dates en UTC. Une cellule qui commence par =, +, - ou @ est précédée d’une apostrophe.
Importer un fichier CSVLien vers la section Importer un fichier CSV
POST / · members:write
Importe jusqu’à 5 000 lignes (2 Mo) : chaque ligne invite une personne, l’ajoute directement (adresse d’un domaine vérifié, organisation non payée par carte) ou met à jour un membre (groupes, étiquettes, rôle, licence ajoutés ; rien n’est jamais retiré).
| Paramètre | Type | Description |
|---|---|---|
dry_run | booléen | true : simulation, rien n’est écrit. |
create_missing | booléen | true : crée les groupes et les étiquettes inconnus. |
Le fichier s’envoie de trois façons : multipart/ avec un champ file, application/ avec {"csv": , ou le CSV brut (text/csv).
| Colonne | Contenu |
|---|---|
email | Obligatoire. |
name | Nom affiché. |
groups | Noms de groupes, séparés par | ou ;. |
labels | clé=valeur, séparées par | ou ;. |
role | member, admin (propriétaire seulement), manager, billing, ou la clé (key) d’un rôle créé. |
license | Identifiant ou forfait d’un lot. |
L’en-tête est reconnu en français ou en anglais (GET / donne les noms acceptés) ; sans en-tête, les colonnes sont prises dans cet ordre. Le séparateur (,, ; ou tabulation) est détecté.
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"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"])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);La réponse donne summary (lignes par action, erreurs, sièges restants), to_create (groupes et étiquettes à créer) et lines : pour chaque ligne, action (invite, add_domain, update, already_member, already_invited ou error), errors et warnings. Une erreur de ligne n’arrête pas l’import : les autres lignes sont traitées.
Hors simulation, une importation de 100 lignes au plus répond tout de suite avec le résultat et un job_id. Au-delà, elle répond 202 avec {"job_id", et continue en arrière-plan. Suivez-la avec GET /, qui rend status (running, done, failed ou interrupted), total, done, phase et, à la fin, result (les lignes en erreur ou en avertissement, 2 000 au plus). Un suivi est gardé 24 heures.
Chaque invitation de l’import compte dans les limites d’invitations ; la simulation ne les vérifie pas.
Voir aussiLien vers la section Voir aussi
- Invitations et codes d’accès
- Comptes des membres
- Webhooks, pour suivre les arrivées et les départs