# POST /partner/webhooks

Enregistrer une adresse HTTPS qui recevra les événements de votre marque, et choisir les types d'événements envoyés à cette adresse. Droit webhooks:write.

Source : https://docs.sealtrust.io/reference/post-partner-webhooks/

---

Vous enregistrez une adresse HTTPS qui recevra les événements de votre marque,
et vous choisissez les types d'événements envoyés à cette adresse.
L'abonnement appartient à la marque de la clef qui appelle. En quittant cette
page, vous saurez quels types d'événements partent réellement aujourd'hui,
comment vérifier la signature d'une livraison, et quelle réponse votre
intégration reçoit à chaque refus.

Adresse complète :

```http
POST https://api.sealtrust.io/v1/partner/webhooks
```

Le même point d'entrée répond aussi sans le préfixe `/v1`, à
`https://api.sealtrust.io/partner/webhooks`. Les deux adresses appellent le
même code et les deux sont permanentes. Utilisez la forme `/v1` pour une
nouvelle intégration.

## Autorisation

Clef d'API dans l'en-tête `Authorization`, au format `Bearer` suivi d'un espace
puis de la clef.

| Ce qui est exigé | Valeur |
| --- | --- |
| Authentification | clef d'API de votre marque |
| Droit porté par la clef | `webhooks:write` |
| Fonctionnalité de votre offre | les notifications par webhook, comprises à partir de l'offre Prestige, absentes de l'offre Essentiel |

Les offres Prestige, Maison et Inside portent les notifications par webhook.
L'offre Essentiel ne les porte pas, et le compte d'essai non plus. Si votre
offre ne les porte pas, vous recevez un 403 sur cette route.

La clef décide de la marque. Vous ne pouvez pas créer un abonnement pour une
autre marque que la sienne, et aucun champ du corps ne permet de la changer.

Un droit manquant renvoie 403 sans rien créer.

> [!ATTENTION] L'offre commande aussi l'envoi des événements
> Nous vérifions l'offre une deuxième fois au moment d'envoyer un événement.
> Une marque qui change d'offre garde ses abonnements, continue à les lire et à
> les supprimer, et ne reçoit plus aucune livraison tant que son offre ne porte
> pas les notifications.

## Plafond d'appels

Nous mesurons le débit sur une fenêtre fixe de 60 secondes. Le plafond dépend
de votre offre, et nous pouvons le relever pour votre marque sans changer votre
abonnement. Lisez la valeur du moment dans l'en-tête `X-RateLimit-Limit` de
chaque réponse acceptée. Quand aucune valeur n'est posée sur votre marque ni
sur votre offre, la valeur de repli est de 120 appels par fenêtre.

Deux compteurs se superposent, avec le même plafond : un par clef, un pour la
somme de toutes les clefs de votre marque. Créer une clef de plus n'augmente
donc pas le débit total autorisé.

| En-tête | Contenu |
| --- | --- |
| `X-RateLimit-Limit` | le plafond appliqué sur la fenêtre |
| `X-RateLimit-Remaining` | ce qu'il vous reste dans la fenêtre en cours |
| `X-RateLimit-Reset` | l'horodatage de fin de la fenêtre, en secondes |
| `X-RateLimit-Scope` | `key` ou `brand`, le compteur le plus contraignant des deux |

Un dépassement renvoie 429 avec ces quatre en-têtes, plus `Retry-After`.
`Retry-After` compte les secondes qui restent dans la fenêtre en cours, et vaut
au minimum 1.

> [!INFO] Cette route ne consomme aucun quota quotidien
> Le quota quotidien de la clef se consomme par article créé et par vente
> déclarée. La création d'un abonnement n'en prend aucune unité. Seul le
> plafond de débit s'applique ici.

## Paramètres de chemin et de requête

Aucun. Ce point d'entrée ne lit ni paramètre de chemin, ni paramètre de
requête. Tout est dans le corps.

## Corps de la requête

Content-Type `application/json`.

| Nom | Type | Obligatoire | Description |
| --- | --- | --- | --- |
| `url` | `string` | oui | L'adresse qui recevra les événements. Elle doit commencer par `https://` et mesurer de 10 à 2048 caractères. |
| `events` | `string[]` | non | Les types d'événements auxquels vous vous abonnez. Chaque valeur doit figurer dans la liste ci-dessous. Absent ou vide, l'abonnement ne reçoit jamais rien. |
| `secret` | `string` | non | Le secret partagé qui signe chaque livraison. 128 caractères au maximum. Absent, les livraisons partent sans signature. |

Le corps n'accepte que ces trois noms. Tout autre champ fait échouer la requête
en 422.

> [!ATTENTION] Un abonnement sans `events` est muet
> `events` est facultatif au sens du contrôle de saisie. Un abonnement dont la
> liste est vide apparaît dans votre liste d'abonnements, se déclare en bonne
> santé, et ne reçoit aucun événement. Nommez toujours au moins un type.

### Les 23 types d'événements acceptés

Un nom absent de ces deux listes fait échouer la requête en 422.

#### Les 17 types que nous livrons aujourd'hui

**Cycle de vie d'un article**

`product.minted`, `product.transferred`, `transfer.accepted`

**Scans et sécurité**

`product.scanned`, `product.gray_market`, `clone.alert`

**Retours et garantie**

`return.requested`, `return.received`, `return.completed`,
`return.rejected`, `return.expired`, `warranty.claimed`

**Rachat**

`buyback.offered`, `buyback.accepted`, `buyback.declined`,
`buyback.completed`, `buyback.expired`

#### Les 6 types acceptés à l'abonnement et jamais émis

`product.burned`, `product.status_changed`, `batch.completed`, `batch.failed`,
`certificate.issued`, `warranty.expiring_soon`

Ces six noms passent le contrôle de saisie et s'inscrivent dans votre
abonnement. Aucun code du produit ne les émet à ce jour, donc votre adresse ne
recevra jamais de livraison portant l'un de ces noms. Ne construisez aucune
alerte ni aucun automatisme dessus. Nous mettrons cette liste à jour le jour où
l'un d'eux partira.

> [!ATTENTION] `product.minted` ne couvre pas les créations par l'API partenaire
> Nous émettons `product.minted` quand un article est créé depuis la console.
> Le chemin de création en lot de l'API partenaire n'émet aucun événement de
> cycle de vie. Un abonnement à `product.minted` reste donc silencieux pendant
> que vous appelez l'API partenaire.

### Ce que fait le secret

Quand vous fournissez un `secret`, chaque livraison porte l'en-tête
`X-Webhook-Signature`, au format `t=<horodatage>,v1=<empreinte>`. L'empreinte
est un HMAC-SHA256 calculé avec votre secret sur la chaîne composée de
l'horodatage, d'un point, puis du corps reçu octet pour octet. Vérifiez la
signature sur les octets bruts que vous recevez, avant de décoder le JSON.

Trois autres en-têtes accompagnent chaque livraison.

| En-tête | Contenu | Couvert par la signature |
| --- | --- | --- |
| `X-Webhook-Event` | le type d'événement livré, par exemple `product.scanned` | non |
| `X-Webhook-Id` | un identifiant stable pour un même événement, qui vous sert à écarter les doublons de renvoi | non |
| `X-Webhook-Timestamp` | l'horodatage utilisé dans la signature, en secondes | oui |

`X-Webhook-Event` vous dit de quel événement il s'agit quand un abonnement
porte sur plusieurs types. Il reste hors de la signature, donc il n'authentifie
rien. Quand votre récepteur doit aiguiller sur une valeur authentifiée,
enregistrez une adresse par type d'événement, et servez-vous de cet en-tête
seulement pour lire vos journaux.

Sans `secret`, l'en-tête `X-Webhook-Signature` est absent de vos livraisons.

> [!DANGER] Le secret ne se relit jamais
> La réponse de création ne contient pas le champ `secret`, et aucune route de
> lecture ne le renvoie. Conservez-le au moment où vous l'envoyez. Si vous le
> perdez, remplacez-le par une modification de l'abonnement, puis reconfigurez
> votre récepteur.

## Requête d'exemple

Les trois programmes font la même chose : ils enregistrent une adresse pour
deux types d'événements réellement livrés, avec un secret de signature.

:::onglets
```bash title="curl"
curl -X POST https://api.sealtrust.io/v1/partner/webhooks \
  -H "Authorization: Bearer st_test_0000000000000000000000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://exemple-sas.example.com/sealtrust/evenements",
    "events": ["product.scanned", "clone.alert"],
    "secret": "secret-partage-a-remplacer"
  }'
```
```typescript title="TypeScript"
import { SealTrustClient } from "@sealtrust-io/sdk";

const sealtrust = new SealTrustClient({
  apiKey: "st_test_0000000000000000000000000000000000000000000000",
});

const abonnement = await sealtrust.webhooks.create({
  url: "https://exemple-sas.example.com/sealtrust/evenements",
  events: ["product.scanned", "clone.alert"],
  secret: "secret-partage-a-remplacer",
});

console.log(abonnement.id, abonnement.health);
```
```python title="Python"
import requests

reponse = requests.post(
    "https://api.sealtrust.io/v1/partner/webhooks",
    headers={
        "Authorization": "Bearer st_test_0000000000000000000000000000000000000000000000",
        "Content-Type": "application/json",
    },
    json={
        "url": "https://exemple-sas.example.com/sealtrust/evenements",
        "events": ["product.scanned", "clone.alert"],
        "secret": "secret-partage-a-remplacer",
    },
    timeout=30,
)

reponse.raise_for_status()
abonnement = reponse.json()
print(abonnement["id"], abonnement["health"])
```
:::

## Réponse d'exemple

Une création réussie répond **201 Created**.

```json title="201 Created"
{
  "id": 41,
  "brand_id": 12,
  "url": "https://exemple-sas.example.com/sealtrust/evenements",
  "events": ["product.scanned", "clone.alert"],
  "is_active": true,
  "health": "healthy",
  "created_at": "2026-08-20T09:14:03.512841+00:00",
  "updated_at": "2026-08-20T09:14:03.512841+00:00"
}
```

| Champ | Type | Ce qu'il contient |
| --- | --- | --- |
| `id` | `integer` | L'identifiant de l'abonnement. C'est lui que vous passerez aux routes de lecture, de modification et de suppression. |
| `brand_id` | `integer` | La marque propriétaire, celle de votre clef. |
| `url` | `string` | L'adresse enregistrée. C'est cette valeur exacte que la suppression vous demandera de recopier. |
| `events` | `string[]` | Les types d'événements souscrits. |
| `is_active` | `boolean` | `true` à la création. Une modification peut le passer à `false` pour éteindre l'abonnement sans le supprimer. |
| `health` | `string` | `healthy` à la création. Passe à `degraded` quand la série de renvois abandonne votre adresse, et revient à `healthy` à la première livraison réussie. |
| `created_at` | `string` | Date et heure de création, au format ISO 8601. |
| `updated_at` | `string` | Date et heure de la dernière modification, au format ISO 8601. |

> [!ATTENTION] Cet appel n'est pas idempotent
> Ce point d'entrée ne lit aucun en-tête `Idempotency-Key`. Le SDK TypeScript
> en envoie un sur chaque POST, et cette route l'ignore. Deux appels identiques
> créent donc deux abonnements distincts, et votre adresse recevra chaque
> événement deux fois. En cas de doute après un délai dépassé, listez vos
> abonnements avant de rappeler.

> [!ATTENTION] L'adresse doit être joignable publiquement
> Le contrôle fait à la création porte sur la forme : le préfixe `https://` et
> la longueur. Nous acceptons donc à l'enregistrement une adresse interne à
> votre réseau, et elle ne recevra jamais rien. Au moment de livrer, nous
> écartons les destinations privées, locales et de bouclage.

## Erreurs

Le corps d'une réponse d'erreur contient un seul champ, `detail`.

| Code | Condition | Ce que vous devez faire |
| --- | --- | --- |
| 401 | L'en-tête `Authorization` est absent. `detail` vaut `"Missing Authorization header"`, et la réponse porte `WWW-Authenticate: Bearer`. | Ajoutez l'en-tête `Authorization: Bearer <votre clef>`. |
| 401 | L'en-tête ne commence pas par `Bearer` suivi d'un espace. `detail` vaut `"Invalid Authorization header format (expected 'Bearer <token>')"`, et la réponse porte `WWW-Authenticate: Bearer`. | Respectez le mot `Bearer`, un espace, puis la clef. |
| 401 | La valeur envoyée est vide ou fait moins de 40 caractères. `detail` vaut `"Invalid API key format"`. | Vérifiez que la clef a été copiée en entier. |
| 401 | La clef ne correspond à aucune clef connue. `detail` vaut `"Invalid API key"`. | La clef est fausse ou a été supprimée. Créez-en une depuis la console de votre marque. |
| 403 | La clef n'est plus active. `detail` reprend son état, par exemple `"API key is revoked"`. | Utilisez une clef active. |
| 403 | La clef a dépassé sa date d'expiration. `detail` vaut `"API key has expired"`. | Créez une nouvelle clef. La clef bascule en expirée dès ce refus. |
| 403 | La clef ne porte pas le droit exigé. `detail` vaut `"Missing required scope: webhooks:write"`. | Créez une clef qui porte ce droit. |
| 403 | Votre offre ne comprend pas les notifications. `detail` est un objet dont `code` vaut `FEATURE_NOT_AVAILABLE` et `feature` vaut `webhooks`. | Passez sur une offre qui les porte. Rejouer la requête ne change rien. |
| 422 | La validation refuse le corps quand : `url` est absente, `url` ne commence pas par `https://`, `url` sort des bornes de 10 à 2048 caractères, un type d'événement est inconnu, `secret` dépasse 128 caractères, ou un champ inconnu est présent. `detail` est une liste, et chaque entrée porte `loc`, `type` et `msg`. | Lisez `loc` pour savoir quelle valeur est refusée, corrigez-la, rappelez. |
| 429 | Votre clef dépasse son plafond de débit. `detail` commence par `Rate limit exceeded`, et `X-RateLimit-Scope` vaut `key`. | Attendez le nombre de secondes indiqué par `Retry-After`, puis rappelez. |
| 429 | La somme des clefs de votre marque dépasse le plafond. `detail` commence par `Brand rate limit exceeded`, et `X-RateLimit-Scope` vaut `brand`. | Attendez `Retry-After`. Créer une clef de plus n'augmente pas ce plafond. |
| 503 | Le service qui tient les compteurs de débit est momentanément indisponible. `detail` vaut `"Rate limiting temporarily unavailable, please retry shortly"`. | Nous n'avons rien créé. Réessayez plus tard. |
| 500 | Une panne de notre côté. `detail` vaut `"Internal Server Error"`, ou `"Internal server error"` quand la panne survient pendant la lecture de votre clef. | Réessayez. Si le refus persiste, envoyez-nous l'en-tête `X-Request-Id` de la réponse. |

> [!INFO] L'ordre des contrôles
> Nous contrôlons dans cet ordre : l'authentification de la clef, dont son état
> actif et sa date d'expiration ; la validation du corps ; le plafond de débit ;
> le droit `webhooks:write` ; l'appartenance des notifications à votre offre. Un
> refus à l'une de ces étapes ne crée rien.
>
> Cet ordre commande votre temporisation. Les deux 403 qui portent sur le droit
> `webhooks:write` et sur votre offre arrivent après le plafond de débit. Leur
> réponse porte donc les en-têtes `X-RateLimit-*` du budget déjà entamé, et vous
> pouvez y lire ce qu'il vous reste avant de rappeler.

## Voir aussi

- [`GET /partner/webhooks`](/reference/get-partner-webhooks/),
  lister vos abonnements aux notifications, page par page.
- [`GET /partner/webhooks/{webhook_id}`](/reference/get-partner-webhooks-id/),
  lire un abonnement et son état de livraison.
- [`PUT /partner/webhooks/{webhook_id}`](/reference/put-partner-webhooks-id/),
  modifier l'adresse, les événements ou le secret d'un abonnement.
- [`DELETE /partner/webhooks/{webhook_id}`](/reference/delete-partner-webhooks-id/),
  supprimer un abonnement et le secret de signature qui lui est attaché.
- [Recevoir les événements par webhook](/webhooks/),
  créer un abonnement, vérifier une signature, rattraper les événements
  perdus.
