# Validation

> Le schéma de requête est fermé : chaque refus de validation, son code, son message exact, et les règles qui surprennent le plus souvent (majuscules, null, UTC, champs inconnus).

Le corps d'une requête est validé contre un schéma **fermé** : un champ inconnu n'est jamais ignoré, il fait échouer la requête. Cette rigueur rend la comparaison de deux requêtes totale : un champ toléré en silence ferait différer deux requêtes identiques aux yeux du moteur, et produirait un `409` injustifié.

Toutes les erreurs de validation du corps rendent `400` avec le code `invalid_request`. Le champ `detail` dit quelle règle a échoué ; la première règle en échec arrête la validation.

```json
{
  "type": "https://developer.noryo.app/errors/invalid_request",
  "title": "Invalid request",
  "status": 400,
  "code": "invalid_request",
  "detail": "currency must be an ISO 4217 code",
  "request_id": "req_…"
}
```

> **Attention** Bâtissez votre logique sur `code`, pas sur `detail`. Le texte de `detail` est écrit pour un humain, en anglais, et peut être reformulé sans changement de version.

## Les messages de `detail`

| `detail` | Cause |
|---|---|
| `body must be a JSON object` | Le corps est un tableau, une chaîne, un nombre ou `null` |
| `unknown field: <nom>` | Un champ hors schéma est présent |
| `descriptor is required` | `descriptor` absent, vide, composé seulement d'espaces, ou pas une chaîne |
| `descriptor is too long` | Plus de 512 caractères |
| `amount must be a finite number` | `amount` n'est pas un nombre JSON (une chaîne `"12.40"`, `null`) |
| `currency must be an ISO 4217 code` | Pas exactement trois lettres majuscules (`eur`, `EURO`, `null`) |
| `occurred_at must be ISO 8601 UTC` | Pas une chaîne de la forme `AAAA-MM-JJTHH:MM:SS` suivie d'une fraction facultative et de `Z`, ou une date qui n'existe pas |
| `country must be ISO 3166-1 alpha-2` | Pas exactement deux lettres majuscules |
| `mcc must be four digits` | Pas une chaîne d'exactement quatre chiffres (un nombre comme `5814` compris) |
| `external_transaction_id must be 1..255 characters` | Vide, trop long, ou pas une chaîne |

## Les règles qui surprennent

**Les champs inconnus sont refusés.** Une faute de frappe (`occured_at`) ou un champ de votre modèle interne envoyé par erreur rend `unknown field: occured_at`. Certains noms sont réservés au serveur et refusés de la même façon s'ils figurent dans le corps : `status`, `confidence`, `billable`, `units`, `environment`, `source`, `customized`, `enrichment_id`, `request_id`, entre autres. Le client ne peut jamais déclarer ce que le serveur calcule.

**`null` n'est pas une absence.** Pour un champ facultatif que vous n'avez pas, omettez la clé. `"amount": null` est refusé.

**Les majuscules sont exigées.** `currency` et `country` sont comparés tels quels ; le validateur refuse `eur` plutôt que de le corriger. Voir [Formats](https://developer.noryo.app/fr-FR/merchant/requests/formats).

**`occurred_at` doit être en UTC, avec `Z`.** `2026-09-06T13:42:00+02:00` est refusé : convertissez en `2026-09-06T11:42:00Z`. Une date bien formée qui n'existe pas (`2026-02-30T10:00:00Z`, une heure à `24`, une seconde à `60`) est refusée de la même façon, en `400`.

**`mcc` est une chaîne.** Envoyez `"5814"`, pas `5814` : un `5814` numérique est refusé en `400` avec `mcc must be four digits`.

## Le corps d'un rafraîchissement

Le corps de `POST …/refresh` accepte les six champs économiques et rien d'autre. Un `external_transaction_id` y est refusé (`unknown field: external_transaction_id`) : le rafraîchissement vise l'enrichissement nommé dans l'URL, et accepter un second identifiant laisserait une même requête désigner deux transactions. Voir [Rafraîchir](https://developer.noryo.app/fr-FR/merchant/refresh).

## Les refus qui précèdent la validation du corps

| Code | Statut | Cause |
|---|---|---|
| [`unsupported_media_type`](https://developer.noryo.app/fr-FR/errors/unsupported_media_type) | 415 | `Content-Type` absent ou différent de `application/json` |
| [`request_entity_too_large`](https://developer.noryo.app/fr-FR/errors/request_entity_too_large) | 413 | Corps de plus de 16 Kio |
| [`invalid_json`](https://developer.noryo.app/fr-FR/errors/invalid_json) | 400 | Corps qui n'est pas du JSON valide, ou corps vide |
| [`invalid_idempotency_key`](https://developer.noryo.app/fr-FR/errors/invalid_idempotency_key) | 400 | En-tête `Idempotency-Key` mal formé |

## Les autres refus propres à la route

| Code | Statut | Cause |
|---|---|---|
| [`insufficient_scope`](https://developer.noryo.app/fr-FR/errors/insufficient_scope) | 403 | La clé n'a pas `enrich:write` |
| [`identifier_required`](https://developer.noryo.app/fr-FR/errors/identifier_required) | 400 | Création sans `Idempotency-Key` ni `external_transaction_id`, ou rafraîchissement sans `Idempotency-Key` |

Sur la création, l'ordre est : corps, scope, identifiant. Sur le rafraîchissement : identifiant de chemin, `Idempotency-Key`, scope, puis corps. L'ordre complet des contrôles est décrit dans [Requêtes](https://developer.noryo.app/fr-FR/merchant/requests).

---

Page canonique : https://developer.noryo.app/fr-FR/merchant/requests/validation
