---
title: "Signalements"
description: "Scan paginé (keyset) de la file de modération hxa_bo.report. Renvoie les données brutes, sans anonymisation : la modération a besoin d'identifier le rappor…"
---

## `GET /admin/reports`

Scan paginé (keyset) de la file de modération `hxa_bo.report`. Renvoie les **données brutes**, sans anonymisation : la modération a besoin d'identifier le rapporteur pour les analyses de pattern (utilisateur qui signale en masse, etc.).

**Query params**

| Param        | Valeurs                                                          | Défaut | Notes |
|--------------|------------------------------------------------------------------|--------|-------|
| `status`     | `pending` \| `reviewed` \| `action_taken` \| `dismissed`         | _aucun_ (tous statuts) | Filtre exact. Une valeur inconnue est silencieusement ignorée (équivaut à _aucun_). |
| `targetType` | `media` \| `user` \| `comment`                                   | _aucun_ | Idem. |
| `limit`      | `1..100`                                                         | `50`   | Borné en dur côté serveur. |
| `cursorAt`   | ISO-8601 datetime                                                | _aucun_ | `created_at` du dernier item de la page précédente. À fournir avec `cursorId` (les deux ou aucun). |
| `cursorId`   | hex (32 chars)                                                   | _aucun_ | id du dernier item — discriminant pour les `created_at` identiques. |

Tri implicite : `created_at DESC, id DESC` (plus récents d'abord).

**Réponse (200)**

```jsonc
{
  "items": [
    {
      "id":                "01a3471992e44c60a8f08321f713635a",
      "reporterUserId":    "ff…",
      "targetType":        "media",
      "targetId":          "ab…",
      "reasonCode":        "spam",
      "details":           "…optional free text…",
      "status":            "pending",
      "resolvedByUserId":  null,
      "resolvedAt":        null,
      "resolutionNote":    null,
      "createdAt":         "2026-06-13T12:34:56+00:00",
      "updatedAt":         "2026-06-13T12:34:56+00:00"
    }
  ],
  "nextCursor": { "at": "2026-06-13T12:34:56+00:00", "id": "01a3…" }
  // `null` quand la page courante contient < `limit` items (= dernière page)
}
```

**Exemple curl**

```bash
# Première page de la file
curl -s -H "Authorization: Bearer $ADMIN_API_TOKEN" \
  "http://hydrogen.dev.com/admin/reports?status=pending&limit=50"

# Page suivante
curl -s -H "Authorization: Bearer $ADMIN_API_TOKEN" \
  "http://hydrogen.dev.com/admin/reports?status=pending&limit=50&cursorAt=2026-06-13T12:34:56%2B00:00&cursorId=01a3471992e44c60a8f08321f713635a"
```

**Erreurs**

| Status | Body | Sens |
|---|---|---|
| `400` | `{ "error": "Both cursorAt and cursorId must be supplied together." }` | une moitié seulement du curseur a été envoyée |
| `400` | `{ "error": "cursorAt is not a valid datetime." }` | parsing Carbon KO |
| `400` | `{ "error": "cursorId is not a valid hex UUID." }` | hex malformé |
| `403` | `{ "error": "..." }` | auth KO |

---


## `GET /admin/reports/{hex}`

Lecture unitaire d'un signalement, en **JSON:API 1.1** (cf. [Convention de format](#convention-de-format--détails-360-en-jsonapi)).

**Réponse (200)** : enveloppe `{ jsonapi, data:{ type:"reports", id, attributes } }`. `data.attributes` reprend les mêmes champs que `items[]` de la liste (`id` remonté au niveau `data.id`, pas dupliqué dans `attributes`) : `reporterUserId`, `targetType`, `targetId`, `reasonCode`, `details`, `status`, `resolvedByUserId`, `resolvedAt`, `resolutionNote`, `createdAt`, `updatedAt`.

```json
{
  "jsonapi": { "version": "1.1" },
  "data": {
    "type": "reports",
    "id": "4f3c1a2b-5d6e-7f80-91a2-b3c4d5e6f700",
    "attributes": {
      "reporterUserId": "…", "targetType": "media", "targetId": "…",
      "reasonCode": "spam", "details": "…", "status": "pending",
      "resolvedByUserId": null, "resolvedAt": null, "resolutionNote": null,
      "createdAt": "2026-06-01T10:00:00+00:00", "updatedAt": "2026-06-01T10:00:00+00:00"
    }
  }
}
```

**Erreurs**

| Status | Body | Sens |
|---|---|---|
| `404` | erreur JSON:API (`errors[].title = "Report not found"`) | row absente ou hex malformé |
| `403` | `{ "error": "..." }` | auth KO |

---


## `PATCH /admin/reports/{hex}`

Transition du verdict modérateur.

**Body**

```jsonc
{
  "status":         "reviewed" | "action_taken" | "dismissed",
  "resolutionNote": "Optional internal note.",   // optionnel
  "resolvedBy":     "<reporterHex>"                // optionnel — id opérateur
}
```

- `status` ne peut pas valoir `pending` (pas de réouverture supportée dans cette itération).
- `resolvedBy` est optionnel : utile quand un opérateur humain valide via un BO ; à omettre pour un job automatisé (la colonne reste `NULL`).
- `resolutionNote` est trim+filtré (vide ⇒ NULL).

**Idempotence** : un PATCH sur une row déjà résolue retourne 200 avec le verdict existant sans toucher la base — le premier verdict modérateur est canonique.

**Réponse (200)** : la row complète mise à jour (même shape que `GET /admin/reports/{hex}`).

**Erreurs**

| Status | Body | Sens |
|---|---|---|
| `400` | `{ "error": "status must be one of: reviewed, action_taken, dismissed." }` | enum invalide |
| `400` | `{ "error": "resolvedBy is not a valid hex UUID." }` | hex malformé |
| `400` | `{ "error": "Body must be a JSON object." }` | JSON KO |
| `404` | `{ "error": "Report not found." }` | row absente |
| `403` | `{ "error": "..." }` | auth KO |

**Notes**

- Le verdict n'envoie **aucune** notification au rapporteur ni à la cible — c'est volontaire, la modération reste invisible côté produit. Si un comportement « décision communiquée à l'utilisateur » est requis plus tard, c'est un side-effect à brancher ici (et à documenter).
- Aucune action automatique sur la cible (suppression de média, ban de compte, …) : la modération applique ces gestes hors de Hydrogen via l'admin métier. Cet endpoint ne fait que figer le verdict côté file.

---
