# POST /partner/dispatch/shipments/{shipment_id}/units

Le scan logistique : rattacher jusqu'à 1000 unités à une commande B2B, un résultat par unité. Droit dispatch:write.

Source : https://docs.sealtrust.io/reference/post-partner-dispatch-shipments-id-units/

---

C'est le scan logistique. Vous envoyez les unités que le préparateur vient de
mettre dans le carton, jusqu'à 1000 par appel, et chacune reçoit son propre
résultat. Un appel de 1000 unités dont 3 sont fausses enregistre les 997 autres
et nomme les 3. Le parcours complet est dans le guide [Rattacher chaque unité à sa commande](/expedition-b2b/).

Cet appel est rejouable : une unité déjà dans la commande répond
`already_in_shipment`, jamais une erreur, et jamais une seconde ligne.

## Autorisation

Clef d'API dans l'en-tête `Authorization`, au format `Bearer`, avec le droit
`dispatch:write`. Une clef sans ce droit reçoit un 403 dont le message nomme le
droit manquant. L'offre de la marque de la clef doit comprendre l'accès API, et
la marque de l'expédition doit être sur l'offre Conformité + Identité.

```http
Authorization: Bearer votre_clef
```

## Plafond d'appels

Le même que les autres routes partenaires : un compteur par clef et un compteur
pour la somme des clefs de votre marque, 120 appels par fenêtre de 60 secondes
par défaut. Chaque appel qui écrit prélève 1 unité du quota journalier de la
clef, quel que soit le nombre d'unités qu'il porte.

## Corps de la requête

| Nom | Type | Obligatoire | Description |
| --- | --- | --- | --- |
| `units` | `array` de `string` | oui | De 1 à 1000 unités, chacune telle que le lecteur l'a lue : le numéro de série de 12 caractères imprimé sur l'étiquette, le lien de l'unité (`https://sealtrust.io/p/{serial}`), son lien GS1 Digital Link (`.../01/{gtin}/21/{serial}`) ou son lien de QR signé. |
| `reallocate` | `boolean` | non | `true` déplace dans cette commande les unités qui sont dans une autre commande de votre marque. Absent ou `false`, une telle unité répond `already_allocated`. |
| `reason` | `string` | avec `reallocate` | Le motif du déplacement, 200 caractères au plus. Il est écrit sur l'ancienne ligne et dans le journal d'audit. |

Une unité appartient à une seule commande active à la fois. La base de données
le garantit, même si deux préparateurs scannent la même unité à la même seconde.

## Requête d'exemple

```bash
curl -i -X POST "https://api.sealtrust.io/v1/partner/dispatch/shipments/318/units" \
  -H "Authorization: Bearer st_live_0000000000000000000000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{"units": ["H897RFWJ4972", "https://sealtrust.io/p/Y5T2VGGF2NP9", "K2M8Q0R4T6V1", "000000000000"]}'
```

## Réponse d'exemple

Code HTTP 200, même quand des unités sont refusées.

```json
{
  "shipment_id": 318,
  "order_ref": "SO-2026-1042",
  "accepted": 2,
  "rejected": 2,
  "units_allocated": 2,
  "results": [
    {"input": "H897RFWJ4972", "status": "allocated", "ok": true, "product_id": 4096, "serial": "H897RFWJ4972", "other_order_ref": null},
    {"input": "https://sealtrust.io/p/Y5T2VGGF2NP9", "status": "allocated", "ok": true, "product_id": 4097, "serial": "Y5T2VGGF2NP9", "other_order_ref": null},
    {"input": "K2M8Q0R4T6V1", "status": "already_allocated", "ok": false, "product_id": 4098, "serial": "K2M8Q0R4T6V1", "other_order_ref": "SO-2026-0998"},
    {"input": "000000000000", "status": "unknown_unit", "ok": false, "product_id": null, "serial": null, "other_order_ref": null}
  ]
}
```

## Le résultat de chaque unité

| `status` | `ok` | Ce que cela veut dire |
| --- | --- | --- |
| `allocated` | `true` | L'unité est maintenant dans cette commande. |
| `already_in_shipment` | `true` | Elle y était déjà. Rien n'a changé : un second scan ou un appel rejoué est sans effet. |
| `reallocated` | `true` | Elle était dans une autre commande, nommée par `other_order_ref`, et vous avez demandé le déplacement. |
| `unreadable` | `false` | La valeur n'est pas un code d'unité : ni numéro de série, ni lien d'unité. Un lien de lot ou de modèle ne désigne pas une unité. |
| `unknown_unit` | `false` | Aucune unité de votre marque ne porte ce code. Une unité d'une autre marque répond exactement la même chose, pour ne jamais confirmer qu'un code existe ailleurs. |
| `not_minted` | `false` | L'unité existe mais n'est pas encore enregistrée sur la chaîne. Réessayez une fois la frappe confirmée. |
| `already_allocated` | `false` | Elle est dans une autre commande de votre marque, nommée par `other_order_ref`. Retirez-la de cette commande, ou rappelez avec `reallocate`. |

## Erreurs

| Code | Condition | Que faire |
| --- | --- | --- |
| 401 | Clef absente, mal formée ou inconnue. | Envoyez `Authorization: Bearer <votre clef>` avec la clef entière. |
| 403 | La clef n'a pas le droit `dispatch:write`. Message `Missing required scopes: dispatch:write`. | Créez une clef qui porte ce droit. |
| 403 | La marque est sur l'offre Conformité. `detail.code` vaut `OFFER_EXCLUDES_IDENTITY` et `detail.feature` vaut `dispatch`. Rien n'est enregistré. | Passez à l'offre Conformité + Identité. Réessayer ne changera rien. |
| 429 | Plafond de débit ou quota journalier atteint. | Attendez le nombre de secondes de `Retry-After`, ou minuit en temps universel pour le quota. |
| 404 | Aucune expédition de votre marque ne porte cet identifiant. Une expédition d'une autre marque répond exactement la même chose. | Vérifiez l'`id` rendu à la création. |
| 409 | L'expédition est close ou annulée. `detail.code` vaut `shipment_not_open`. | Une expédition close ne change plus. Créez une nouvelle commande. |
| 400 | `reallocate` sans `reason`. `detail.code` vaut `reason_required`. | Ajoutez un motif. |
| 422 | Plus de 1000 unités, ou corps invalide. | Découpez l'envoi en appels de 1000 unités au plus. |
