---
title: "Newsletter"
description: "Visibilité back-office sur la liste de diffusion (jusqu'ici aucune). Renvoie un bloc counts (répartition par statut, toujours présent) et un export paginé…"
---

## `GET /admin/newsletter/subscribers`

Visibilité back-office sur la liste de diffusion (jusqu'ici **aucune**). Renvoie un bloc `counts` (répartition par statut, toujours présent) **et** un export paginé (keyset) filtrable par `status`. Pour récupérer la liste de diffusion réelle, filtrer `?status=confirmed`.

> **Sécurité** : les hash de tokens (confirm / unsubscribe) ne sont **jamais** exposés — ce sont des secrets serveur qui ouvriraient une surface sur les liens un-clic. Lecture seule, via un service dédié (`NewsletterSubscriberDirectory`).

**Query params**

| Param | Valeurs | Défaut | Notes |
|---|---|---|---|
| `status` | `pending` \| `confirmed` \| `unsubscribed` | _tous_ | valeur invalide = `400` (pas d'ignore silencieux) |
| `limit` | `1..500` | `100` | orienté export, borné en dur |
| `cursorAt` | ISO-8601 | _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` (inscriptions les plus récentes). Index dédié `idx_status_created (status, created_at)`.

**Réponse (200)**

```jsonc
{
  "counts": { "pending": 3, "confirmed": 42, "unsubscribed": 5, "total": 50 },
  "items": [
    {
      "id":             "d26d1600cde54bd095e09f8b68ace05f",
      "email":          "jane@example.com",
      "status":         "confirmed",
      "locale":         "fr",
      "source":         "footer",
      "userId":         "ab…",          // hex ou null (inscrit relié à un compte)
      "ipAddress":      "203.0.113.7",   // ou null
      "userAgent":      "Mozilla/5.0 …", // ou null
      "confirmedAt":    "2026-06-18T10:05:00+00:00",
      "unsubscribedAt": null,
      "createdAt":      "2026-06-18T10:00:00+00:00",
      "updatedAt":      "2026-06-18T10:05:00+00:00"
    }
  ],
  "nextCursor": { "at": "2026-06-18T10:00:00+00:00", "id": "d26d…" }
  // `null` quand la page courante contient < `limit` items (= dernière page)
}
```

> `counts` reflète **toute** la table (non filtré par `status`), `items` reflète le filtre courant : un export `?status=confirmed` montre 42 items même si `counts.total` vaut 50.

**Erreurs**

| Status | Body | Sens |
|---|---|---|
| `400` | `{ "error": "Query parameter 'status' must be one of: pending, confirmed, unsubscribed." }` | statut inconnu |
| `400` | `{ "error": "Both cursorAt and cursorId must be supplied together." }` | demi-curseur |
| `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 |

**Exemple curl**

```bash
# Compteurs + export de la liste de diffusion confirmée
curl -s -H "Authorization: Bearer $ADMIN_API_TOKEN" \
  "http://hydrogen.dev.com/admin/newsletter/subscribers?status=confirmed&limit=500"

# Page suivante
curl -s -H "Authorization: Bearer $ADMIN_API_TOKEN" \
  "http://hydrogen.dev.com/admin/newsletter/subscribers?status=confirmed&limit=500&cursorAt=2026-06-18T10:00:00%2B00:00&cursorId=d26d1600cde54bd095e09f8b68ace05f"
```

---
