---
title: "Notifications"
description: "Pousse une annonce éditée par l'admin — soit ciblée sur une liste de destinataires, soit en broadcast à tous les utilisateurs. Rejoint le pipeline standard…"
---

## `POST /admin/notifications`

Pousse une **annonce éditée par l'admin** — soit ciblée sur une liste de destinataires, soit en **broadcast** à tous les utilisateurs. Rejoint le pipeline standard : la ligne est persistée immédiatement, les préférences in-app sont honorées au dispatch, et le cron digest pousse la notification au tick suivant (individuelle ou en digest).

> Le texte est **par locale** et figé tel quel : chaque destinataire est rendu dans **sa** locale (fallback locale par défaut → première fournie). Aucune clé de catalogue : `title`/`body` sont fournis par l'admin et gelés dans `data.translations`.

**Corps (JSON)**

| Champ | Type | Obligatoire | Notes |
|---|---|---|---|
| `translations` | `{ "<locale>": { "title", "body" } }` | oui | locales **supportées** uniquement ; `title`/`body` non vides (title ≤ 150, body ≤ 2000) ; **doit inclure la locale par défaut** `fr-FR` (fallback universel) |
| `url` | string | non | deep-link gelé dans `data.url` (payload push) |
| `userIds` | `["<hex>", …]` | _exactement un mode_ | ciblé, 1..1000 ids |
| `scope` | `"all"` | _exactement un mode_ | broadcast keyset sur tous les users |
| `announcementId` | hex (32) | non | namespace de dedup partagé ; généré et renvoyé si absent |

Fournir **exactement un** de `userIds[]` ou `scope:"all"` (les deux ou aucun = `400`).

**Dedup / idempotence** : tous les destinataires d'une même annonce partagent la clé `system.announcement:<announcementId>`. En broadcast, **renvoyer le `announcementId` retourné** sur chaque appel `?cursor=` suivant pour qu'une page reprise/rejouée ne notifie pas deux fois un même destinataire.

**Query params (broadcast uniquement)**

| Param | Valeurs | Défaut | Notes |
|---|---|---|---|
| `cursor` | hex (32) | _début_ | dernier `lastId` traité par l'appel précédent |
| `batchSize` | `1..1000` | `200` | taille de lot par appel |

**Réponse — ciblé (200)**

```jsonc
{
  "announcementId": "9f1c…",
  "mode":           "targeted",
  "dispatched":     12,                       // lignes persistées
  "skipped":        1,                        // destinataire opt-out in-app
  "failed":         [{ "userId": "ab…", "error": "…" }]
}
```

**Réponse — broadcast (200)** — un lot keyset par appel, boucler jusqu'à `done:true`

```jsonc
{
  "announcementId": "9f1c…",
  "mode":           "broadcast",
  "dispatched":     200,
  "skipped":        0,
  "failed":         [],
  "lastId":         "<hex>",
  "nextCursor":     "<hex>",                  // null quand done
  "done":           false,
  "totalAll":       15234,                    // COUNT(*) users (snapshot)
  "durationMs":     38
}
```

> Comme les autres batches admin, une erreur de dispatch par destinataire **n'avorte pas** la boucle : elle part dans `failed[]`.

**Erreurs**

| Status | Body | Sens |
|---|---|---|
| `400` | `{ "error": "Request body must be a JSON object." }` | corps absent/non-objet |
| `400` | `{ "error": "Provide exactly one of userIds[] or scope:\"all\"." }` | ciblage ambigu |
| `400` | `{ "error": "Invalid cursor." }` / `"Invalid batchSize."` | pagination broadcast KO |
| `422` | `{ "error": "translations must include the default locale \"fr-FR\" (used as fallback)." }` | wording invalide (exemples : locale non supportée, title/body vide, trop long, défaut manquant) |
| `422` | `{ "error": "userIds must be a non-empty array of hex user ids." }` | liste ciblée vide/malformée |
| `403` | `{ "error": "..." }` | auth KO |

**Exemples curl**

```bash
# Ciblé : 2 destinataires, texte bilingue + deep-link
curl -s -X POST -H "Authorization: Bearer $ADMIN_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "translations": {
          "fr-FR": { "title": "Nouveauté", "body": "Découvrez les nouvelles villes." },
          "en-US": { "title": "What'\''s new", "body": "Discover the new cities." }
        },
        "url": "https://hydrogen.app/cities",
        "userIds": ["ab12…", "cd34…"]
      }' \
  http://hydrogen.dev.com/admin/notifications

# Broadcast : 1er appel (announcementId généré et renvoyé)
curl -s -X POST -H "Authorization: Bearer $ADMIN_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "translations": { "fr-FR": { "title": "Maintenance", "body": "Service indisponible 22h-23h." } }, "scope": "all" }' \
  "http://hydrogen.dev.com/admin/notifications?batchSize=500"

# Broadcast : appels suivants — réutiliser announcementId + nextCursor jusqu'à done:true
curl -s -X POST -H "Authorization: Bearer $ADMIN_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "translations": { "fr-FR": { "title": "Maintenance", "body": "Service indisponible 22h-23h." } }, "scope": "all", "announcementId": "9f1c…" }' \
  "http://hydrogen.dev.com/admin/notifications?batchSize=500&cursor=<lastId>"
```

---
