# Membres

> Lister et filtrer les membres, lire une fiche, suspendre, réactiver ou retirer, agir en masse, exporter et importer un fichier CSV avec l’API d’administration.

Toutes les routes sont sous `https://chat.realmoon.ai/api/v1/orgs/{org_id}`. 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](https://realmoon.ai/documentation/api-organisations/invitations/).

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 forbidden`).

## L’objet membre

```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": "<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 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 invalid_request` ; `license` sur une organisation sans lots répond `400 licenses_unavailable`.

```bash title="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 title="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 title="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);
```

## Lire la fiche d’un membre

`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 }
}
```

| 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 base

`PATCH /members/{user_id}` · `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.

```bash title="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 title="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 title="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);
```

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 membre

`DELETE /members/{user_id}` · `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": true}`. 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](https://realmoon.ai/documentation/api-organisations/comptes/#depart).

## Agir sur plusieurs membres

`POST /members/bulk` · `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. |

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

`GET /members/export` · `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 :

```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;;<forfait>;active;2026-01-01 00:00;2026-12-23 08:40
```

Les 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 CSV

`POST /members/import` · `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/form-data` avec un champ `file`, `application/json` avec `{"csv": "<texte>"}`, 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 /members/import/columns` donne les noms acceptés) ; sans en-tête, les colonnes sont prises dans cet ordre. Le séparateur (`,`, `;` ou tabulation) est détecté.

```bash title="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 title="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 title="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);
```

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", "status": "running", "total"}` et continue en arrière-plan. Suivez-la avec `GET /members/import/{job_id}`, 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](https://realmoon.ai/documentation/api-organisations/invitations/#limites) ; la simulation ne les vérifie pas.

## Voir aussi

- [Invitations et codes d’accès](https://realmoon.ai/documentation/api-organisations/invitations/)
- [Comptes des membres](https://realmoon.ai/documentation/api-organisations/comptes/)
- [Webhooks](https://realmoon.ai/documentation/api-organisations/webhooks/#evenements), pour suivre les arrivées et les départs

---

Documentation Moon AI › API des organisations › Membres. Page : https://realmoon.ai/documentation/api-organisations/membres/. Modifiée le 5 novembre 2026.

© 2026 Stellarr Studio SAS. Tous droits réservés. Moon AI, Moon Blur et Stellarr Studio sont des marques déposées à l’INPI.
