# PUT /partner/webhooks/{webhook_id}

Modifier un abonnement aux notifications : son adresse de destination, sa liste d'événements, son secret de signature, son activation. Droit webhooks:write.

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

---

Vous modifiez un abonnement aux notifications que votre marque possède déjà, et
vous recevez l'abonnement dans son nouvel état.

## Autorisation

Envoyez votre clef d'API dans l'en-tête `Authorization`, au format `Bearer`
suivi d'un espace puis de la clef. La clef doit porter le droit
`webhooks:write`. Une clef qui ne porte pas ce droit reçoit un 403 dont le
message nomme le droit manquant.

| Ce que nous exigeons | Valeur |
| --- | --- |
| Authentification | clef d'API de votre marque |
| Droit porté par la clef | `webhooks:write` |
| Fonctionnalité de votre offre | les notifications par webhook |

Deux conditions s'ajoutent au droit de la clef.

- L'abonnement doit appartenir à la marque de la clef. Nous cherchons
  l'abonnement par son numéro **et** par votre marque. Quand cette recherche ne
  trouve rien, vous recevez un 404. Un abonnement qui appartient à une autre
  marque donne exactement la même réponse. Cette route ne vous apprend donc
  jamais qu'un numéro existe ailleurs.
- L'offre de votre marque doit comprendre les notifications. Sinon vous recevez
  un 403 portant le code `FEATURE_NOT_AVAILABLE`. Une exception existe, décrite
  juste en dessous.

> [!INFO] Éteindre un abonnement reste possible sans l'offre
> Nous acceptons un appel dont le corps contient exactement
> `{"is_active": false}` et rien d'autre, même quand votre offre ne comprend
> plus les notifications. Vous gardez ainsi le moyen d'arrêter proprement un
> abonnement que vous ne payez plus. Dès qu'un autre champ accompagne cette
> extinction, ou dès que vous repassez `is_active` à `true`, nous exigeons de
> nouveau l'offre.

L'adresse complète de ce point d'entrée est
`https://api.sealtrust.io/v1/partner/webhooks/{webhook_id}`. Le même point
d'entrée répond aussi sans le préfixe `/v1`, à
`https://api.sealtrust.io/partner/webhooks/{webhook_id}`. Les deux adresses
appellent le même code. Utilisez la forme `/v1` pour une nouvelle intégration.

## Plafond d'appels

Nous mesurons le débit sur une fenêtre fixe de 60 secondes. Le plafond monte
avec votre offre, et nous pouvons poser une valeur sur votre marque qui gagne
sur celle de votre offre. Nous lisons donc, dans cet ordre : la valeur posée
sur votre marque, puis celle de votre offre, puis une valeur de repli de 120
appels par fenêtre. Cette valeur de repli ne sert que si votre marque et votre
offre n'en portent aucune.

Ne devinez pas votre plafond du moment. Vous le lisez sur chaque réponse qui
franchit le contrôle de débit.

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 des clefs supplémentaires
n'augmente donc pas le débit total autorisé.

Quatre en-têtes décrivent le compteur le plus contraignant des deux.

| 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 qui a servi de référence |

> [!ATTENTION] Ces quatre en-têtes manquent sur certaines réponses
> Vous les recevez sur une réponse 200, 403, 404 et 429. Vous ne les recevez
> **pas** sur une réponse 401, 422, 500 ni 503. Lisez donc ces en-têtes avec
> une lecture qui tolère leur absence, sinon votre code s'arrêtera sur la
> première erreur d'authentification.

Un refus de débit renvoie 429 avec `Retry-After`, exprimé en secondes restantes
dans la fenêtre en cours. Cette valeur n'est jamais inférieure à 1.

Ce point d'entrée ne consomme aucun quota quotidien de clef et aucun quota
mensuel d'offre. Le plafond de débit est le seul compteur qui s'applique ici.

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

| Nom | Type | Obligatoire | Description |
| --- | --- | --- | --- |
| `webhook_id` | `integer` | oui | Le numéro de l'abonnement à modifier, dans le chemin. Une valeur qui n'est pas un entier donne un 422. |

Ce point d'entrée n'a aucun paramètre de requête.

### En-têtes

| Nom | Type | Obligatoire | Description |
| --- | --- | --- | --- |
| `Authorization` | `string` | oui | `Bearer` suivi d'un espace puis de votre clef d'API. |
| `Content-Type` | `string` | oui | `application/json`. |
| `Idempotency-Key` | `string` | non | Ce point d'entrée ne déclare pas cet en-tête. Il ne le lit pas et ne le mémorise pas. |

## Corps de la requête

Le corps est un objet JSON. Les quatre champs sont facultatifs. Envoyez
uniquement les champs que vous voulez changer. Un champ absent du corps garde
sa valeur.

| Nom | Type | Obligatoire | Description |
| --- | --- | --- | --- |
| `url` | `string` | non | La nouvelle adresse de destination, de 10 à 2048 caractères. Elle doit commencer par `https://`. N'envoyez jamais `null` dans ce champ, lisez l'avertissement ci-dessous. |
| `events` | `array` de `string` | non | La nouvelle liste des types d'événements. Elle remplace l'ancienne liste en entier. Chaque valeur doit appartenir à la liste ci-dessous. |
| `is_active` | `boolean` | non | `true` pour recevoir les événements, `false` pour arrêter les livraisons sans détruire l'abonnement. Ce champ accepte `true` ou `false`. Omettez-le si vous ne voulez pas le changer. |
| `secret` | `string` | non | Le nouveau secret de signature, jusqu'à 128 caractères. Nous l'utilisons pour calculer l'en-tête `X-Webhook-Signature` de chaque livraison. `null` efface le secret, et les livraisons suivantes partent alors sans signature. |

Le serveur refuse tout champ que ce tableau ne nomme pas. Un champ inconnu fait
échouer la requête en 422, et nous ne modifions rien.

Un corps vide, écrit `{}`, est valide. Il ne modifie rien, il renvoie
l'abonnement inchangé, et nous exigeons l'offre comme pour n'importe quelle
autre modification.

> [!DANGER] N'envoyez jamais `"url": null`
> Une adresse de destination est toujours une chaîne qui commence par
> `https://`. Pour arrêter les livraisons sans détruire l'abonnement, envoyez
> `{"is_active": false}`. Après chaque modification, relisez le champ `url` de
> la réponse : c'est lui qui dit l'adresse que nous appellerons désormais.

> [!DANGER] Nous refusons le champ `description`
> Nous acceptions ce champ puis nous le jetions sans l'enregistrer nulle part.
> Vous croyiez décrire votre abonnement, et vous ne décriviez rien. Il fait
> aujourd'hui échouer la requête en 422. Retirez-le de votre code avant votre
> prochain appel.

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

Une valeur de `events` qui n'est pas dans cette liste fait échouer la requête en
422, et le message nomme les valeurs refusées.

| Famille | Types |
| --- | --- |
| Cycle de vie du produit | `product.minted`, `product.transferred`, `product.burned`, `product.status_changed` |
| Lots | `batch.completed`, `batch.failed` |
| Certificat | `certificate.issued` |
| Distribution et sécurité | `product.scanned`, `product.gray_market`, `clone.alert`, `transfer.accepted` |
| Retours | `return.requested`, `return.received`, `return.completed`, `return.rejected`, `return.expired` |
| Garantie | `warranty.claimed`, `warranty.expiring_soon` |
| Rachat | `buyback.offered`, `buyback.accepted`, `buyback.declined`, `buyback.completed`, `buyback.expired` |

> [!ATTENTION] Une liste vide coupe toutes les livraisons
> Nous acceptons `"events": []`, et `"events": null` produit le même résultat :
> la liste enregistrée devient vide. L'abonnement reste visible, il reste
> `is_active`, et il ne reçoit plus jamais rien. Nous ne livrons un événement
> que si son type figure dans la liste de l'abonnement.

> [!ATTENTION] La signature dépend du secret, et le secret peut disparaître
> Une livraison porte l'en-tête `X-Webhook-Signature` uniquement si
> l'abonnement porte un secret. Trois conséquences.
>
> - Vous envoyez un nouveau secret : nous signons les livraisons suivantes avec
>   ce nouveau secret. Mettez à jour votre serveur de réception avant d'envoyer
>   cet appel, sinon il rejettera les livraisons.
> - Vous envoyez `"secret": null` : nous effaçons le secret, et les livraisons
>   suivantes partent sans aucune signature.
> - L'abonnement n'a jamais porté de secret : ses livraisons n'en portent pas
>   non plus, et cet appel n'y change rien tant que vous n'envoyez pas de
>   secret.
>
> La réponse ne contient jamais le secret. Vous ne pouvez donc pas vérifier par
> cette route si un secret est posé.

## Requête d'exemple

Vous repointez l'abonnement numéro `128` vers une nouvelle adresse et vous
réduisez sa liste à deux types d'événements.

:::onglets
```bash title="curl"
curl -i -X PUT https://api.sealtrust.io/v1/partner/webhooks/128 \
  -H "Authorization: Bearer st_test_0000000000000000000000000000000000000000000000" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://exemple-sas.example.com/sealtrust/evenements",
    "events": ["product.minted", "batch.completed"]
  }'
```
```typescript
import { SealTrustClient } from "@sealtrust-io/sdk";

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

const abonnement = await sealtrust.webhooks.update(128, {
  url: "https://exemple-sas.example.com/sealtrust/evenements",
  events: ["product.minted", "batch.completed"],
});

console.log(abonnement.id, abonnement.url, abonnement.events);
```
```python
import requests

response = requests.put(
    "https://api.sealtrust.io/v1/partner/webhooks/128",
    headers={
        "Authorization": "Bearer st_test_0000000000000000000000000000000000000000000000",
    },
    json={
        "url": "https://exemple-sas.example.com/sealtrust/evenements",
        "events": ["product.minted", "batch.completed"],
    },
    timeout=30,
)

print(response.status_code)
print(response.headers.get("X-RateLimit-Remaining"))
print(response.json())
```
:::

## Réponse d'exemple

Code HTTP `200`.

```json
{
  "id": 128,
  "brand_id": 12,
  "url": "https://exemple-sas.example.com/sealtrust/evenements",
  "events": ["product.minted", "batch.completed"],
  "is_active": true,
  "health": "healthy",
  "created_at": "2026-08-18T09:12:44.512038+00:00",
  "updated_at": "2026-08-20T14:03:21.884517+00:00"
}
```

La réponse compte huit champs et rien d'autre. Le secret n'y figure jamais.

| Champ | Type | Description |
| --- | --- | --- |
| `id` | `integer` | Le numéro de l'abonnement. |
| `brand_id` | `integer` | Le numéro de votre marque. |
| `url` | `string` | L'adresse de destination enregistrée. Relisez-la après chaque modification. |
| `events` | `array` de `string` | La liste des types d'événements enregistrée. |
| `is_active` | `boolean` | `true` si l'abonnement reçoit les événements. |
| `health` | `string` | `healthy` ou `degraded`. Vaut `degraded` après l'échec définitif d'une livraison, et revient à `healthy` à la première livraison réussie. |
| `created_at` | `string` | La date de création de l'abonnement, en temps universel. |
| `updated_at` | `string` | La date de la dernière modification, en temps universel. |

> [!INFO] La liste des abonnements ne rend pas la même forme
> `GET /v1/partner/webhooks` nomme la liste des types d'événements
> `event_types`. Cette route, comme la création et la lecture unitaire, la
> nomme `events`. Les deux portent le même contenu. Prévoyez les deux noms si
> vous lisez les deux points d'entrée avec le même code.

## Erreurs

Le corps d'une réponse d'erreur porte un champ `detail`. Selon le cas, ce champ
contient une phrase, une liste ou un objet.

| Code | Condition | Que faire |
| --- | --- | --- |
| 401 | L'en-tête `Authorization` est absent. `detail` vaut `Missing Authorization header`. La réponse porte aussi `WWW-Authenticate: Bearer`. | Ajoutez l'en-tête. |
| 401 | L'en-tête `Authorization` ne commence pas par `Bearer` suivi d'un espace. La réponse porte aussi `WWW-Authenticate: Bearer`. | Corrigez la forme de l'en-tête. |
| 401 | La clef envoyée est vide, ou fait moins de 40 caractères. `detail` vaut `Invalid API key format`. | Envoyez la clef entière. Une clef tronquée à l'affichage donne cette réponse. |
| 401 | La clef envoyée est inconnue. `detail` vaut `Invalid API key`. | Vérifiez que vous envoyez la clef complète, sans espace ni retour à la ligne. |
| 403 | La clef n'est plus active, parce qu'elle a été révoquée. `detail` nomme son état. | Créez une nouvelle clef dans la console. |
| 403 | La clef a atteint sa date d'expiration. `detail` vaut `API key has expired`. | Créez une nouvelle clef. L'ancienne ne redeviendra jamais valide. |
| 403 | La clef ne porte pas le droit `webhooks:write`. `detail` vaut `Missing required scope: webhooks:write`. | Créez une clef portant ce droit. Les droits d'une clef existante ne se modifient pas. |
| 403 | L'offre de votre marque ne comprend pas les notifications. `detail` vaut `{"code": "FEATURE_NOT_AVAILABLE", "feature": "webhooks"}`. | Changez d'offre depuis la console, ou contactez-nous. Vous pouvez toujours éteindre l'abonnement avec un corps réduit à `{"is_active": false}`. |
| 404 | Aucun abonnement ne porte ce numéro pour votre marque. `detail` vaut `Webhook subscription not found`. | Vérifiez le numéro avec `GET /v1/partner/webhooks`. Vous recevez la même réponse si l'abonnement appartient à une autre marque. |
| 422 | Le `webhook_id` du chemin n'est pas un entier. | Envoyez le numéro rendu par `GET /v1/partner/webhooks`. |
| 422 | Le `webhook_id` du chemin est un entier trop grand pour être un numéro d'abonnement. Le message parle de paramètre de requête, alors que la valeur fautive est dans le chemin. | Envoyez le numéro rendu par `GET /v1/partner/webhooks`. |
| 422 | Le corps contient un champ que ce point d'entrée ne connaît pas. | Retirez ce champ. Nous n'acceptons que `url`, `events`, `is_active` et `secret`. |
| 422 | `url` est une chaîne qui ne commence pas par `https://`. | Corrigez l'adresse pour qu'elle commence par `https://`. |
| 422 | `url` est une chaîne de moins de 10 ou de plus de 2048 caractères. | Corrigez la longueur de l'adresse. |
| 422 | `events` contient au moins un type inconnu. Le message nomme les valeurs refusées. | Reprenez les valeurs du tableau des 23 types d'événements. |
| 422 | `secret` dépasse 128 caractères. | Raccourcissez le secret. |
| 422 | Un champ ne porte pas le type annoncé, par exemple `url` reçoit un nombre ou `is_active` reçoit une chaîne. | Corrigez le type. Le message nomme le champ fautif. |
| 422 | Le corps n'est pas un objet JSON, ou le corps est absent. | Envoyez un objet JSON, même vide, avec `Content-Type: application/json`. |
| 429 | Le plafond de débit de la clef est atteint. Les en-têtes `Retry-After` et la famille `X-RateLimit-*` accompagnent la réponse, avec `X-RateLimit-Scope: key`. | Attendez le nombre de secondes indiqué par `Retry-After`, puis réessayez. |
| 429 | Le plafond de débit de la marque est atteint, toutes clefs confondues. `X-RateLimit-Scope` vaut `brand`. | Attendez le nombre de secondes indiqué par `Retry-After`. Créer une clef supplémentaire ne relève pas ce plafond. |
| 500 | Une erreur inattendue s'est produite. `detail` vaut `Internal Server Error`. | Réessayez. Si l'erreur persiste, contactez le support en donnant l'en-tête `X-Request-Id` de la réponse. |
| 503 | Le service de plafonnement des appels est momentanément indisponible. Nous refusons l'appel avant toute lecture. | Réessayez dans quelques instants. L'abonnement n'a pas été modifié. |

> [!INFO] Un refus ne modifie jamais l'abonnement à moitié
> Nous n'enregistrons la modification qu'une fois tous les contrôles passés :
> la clef, le corps de la requête, le droit `webhooks:write`, l'existence de
> l'abonnement dans votre marque et l'offre de votre marque. Un 401, un 403,
> un 404, un 422, un 429 ou un 503 laisse donc l'abonnement exactement dans
> l'état où il était.

## Voir aussi

- [`GET /partner/webhooks/{webhook_id}`](/reference/get-partner-webhooks-id/),
  lire un abonnement et son état de livraison.
- [`GET /partner/webhooks`](/reference/get-partner-webhooks/),
  lister vos abonnements aux notifications, page par page.
- [`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.
