Method POST/partner /webhooks
Register an HTTPS address that will receive the events of your brand, and choose the event types sent to that address. Scope webhooks:write.
On this page
You register an HTTPS address that will receive the events of your brand, and you choose the event types sent to that address. The subscription belongs to the brand of the key that calls. By the end of this page, you will know which event types actually go out today, how to verify the signature of a delivery, and which response your integration receives at every refusal.
Full address:
POST https://api.sealtrust.io/v1/partner/webhooksThe same endpoint also answers without the /v1 prefix, at
https://api.sealtrust.io/partner/webhooks. The two addresses call the
same code and both are permanent. Use the /v1 form for a
new integration.
#
API key in the Authorization header, in the Bearer format followed by a space
then the key.
| What is required | Value |
|---|---|
| Authentication | API key of your brand |
| Scope carried by the key | webhooks:write |
| Feature of your plan | webhook notifications, included from the Prestige plan upward, absent from the Essentiel plan |
The Prestige, Maison and Inside plans carry webhook notifications. The Essentiel plan does not carry them, and neither does the trial account. If your plan does not carry them, you receive a 403 on this route.
The key decides the brand. You cannot create a subscription for a brand other than its own, and no field of the body allows changing it.
A missing scope returns 403 without creating anything.
#Rate limit
We measure the rate over a fixed 60-second window. The cap depends
on your plan, and we can raise it for your brand without changing your
subscription. Read the current value in the X-RateLimit-Limit header of
every accepted response. When no value is set on your brand or
on your plan, the fallback value is 120 calls per window.
Two counters are layered, with the same cap: one per key, one for the sum of all the keys of your brand. Creating one more key therefore does not increase the total rate allowed.
| Header | Content |
|---|---|
X-RateLimit-Limit | the cap applied over the window |
X-RateLimit-Remaining | what is left to you in the current window |
X-RateLimit-Reset | the end-of-window timestamp, in seconds |
X-RateLimit-Scope | key or brand, the more constraining of the two counters |
Exceeding it returns 429 with these four headers, plus Retry-After.
Retry-After counts the seconds that remain in the current window, and is
at minimum 1.
#Path and query parameters
None. This endpoint reads neither path parameter, nor query parameter. Everything is in the body.
#Request body
Content-Type application/json.
| Name | Type | Required | Description |
|---|---|---|---|
url | string | yes | The address that will receive the events. It must start with https:// and be between 10 and 2048 characters long. |
events | string[] | no | The event types you subscribe to. Each value must appear in the list below. Absent or empty, the subscription never receives anything. |
secret | string | no | The shared secret that signs every delivery. 128 characters at most. Absent, the deliveries go out without a signature. |
The body accepts only these three names. Any other field makes the request fail with a 422.
#The 23 accepted event types
A name absent from these two lists makes the request fail with a 422.
#The 17 types we deliver today
Life cycle of an item
product.minted, product.transferred, transfer.accepted
Scans and security
product.scanned, product.gray_market, clone.alert
Returns and warranty
return.requested, return.received, return.completed,
return.rejected, return.expired, warranty.claimed
Buyback
buyback.offered, buyback.accepted, buyback.declined,
buyback.completed, buyback.expired
#The 6 types accepted at subscription and never emitted
product.burned, product.status_changed, batch.completed, batch.failed,
certificate.issued, warranty.expiring_soon
These six names pass input validation and are recorded in your subscription. No code in the product emits them to this day, so your address will never receive a delivery carrying one of these names. Build no alert and no automation on them. We will update this list the day one of them goes out.
#What the secret does
When you supply a secret, every delivery carries the header
X-Webhook-Signature, in the format t=<horodatage>,v1=<empreinte>. The hash
is an HMAC-SHA256 computed with your secret over the string made of
the timestamp, a dot, then the body received byte for byte. Verify
the signature on the raw bytes you receive, before decoding the JSON.
Three other headers come with every delivery.
| Header | Content | Covered by the signature |
|---|---|---|
X-Webhook-Event | the event type delivered, for example product.scanned | no |
X-Webhook-Id | a stable identifier for a given event, which you use to discard duplicates from resends | no |
X-Webhook-Timestamp | the timestamp used in the signature, in seconds | yes |
X-Webhook-Event tells you which event it is when a subscription
covers several types. It stays outside the signature, so it authenticates
nothing. When your receiver must route on an authenticated value,
register one address per event type, and use this header
only to read your logs.
Without a secret, the X-Webhook-Signature header is absent from your deliveries.
#Example request
The three programs do the same thing: they register one address for two event types that are actually delivered, with a signing secret.
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"
}'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);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"])#Example response
A successful creation answers 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"
}| Field | Type | What it contains |
|---|---|---|
id | integer | The identifier of the subscription. It is the one you will pass to the read, modify and delete routes. |
brand_id | integer | The owning brand, the one of your key. |
url | string | The address registered. It is this exact value that the deletion will ask you to copy back. |
events | string[] | The event types subscribed to. |
is_active | boolean | true at creation. A modification can switch it to false to turn the subscription off without deleting it. |
health | string | healthy at creation. Switches to degraded when the series of resends gives up on your address, and comes back to healthy at the first successful delivery. |
created_at | string | Date and time of creation, in ISO 8601 format. |
updated_at | string | Date and time of the last modification, in ISO 8601 format. |
#Errors
The body of an error response contains a single field, detail.
| Code | Condition | What you must do |
|---|---|---|
| 401 | The Authorization header is missing. detail is "Missing Authorization header", and the response carries WWW-Authenticate: Bearer. | Add the Authorization: Bearer <votre clef> header. |
| 401 | The header does not start with Bearer followed by a space. detail is "Invalid Authorization header format (expected 'Bearer <token>')", and the response carries WWW-Authenticate: Bearer. | Respect the word Bearer, a space, then the key. |
| 401 | The value sent is empty or is shorter than 40 characters. detail is "Invalid API key format". | Check that the key was copied in full. |
| 401 | The key matches no known key. detail is "Invalid API key". | The key is wrong or was deleted. Create one from the console of your brand. |
| 403 | The key is no longer active. detail repeats its state, for example "API key is revoked". | Use an active key. |
| 403 | The key has passed its expiration date. detail is "API key has expired". | Create a new key. The key switches to expired as of this refusal. |
| 403 | The key does not carry the required scope. detail is "Missing required scope: webhooks:write". | Create a key that carries this scope. |
| 403 | Your plan does not include notifications. detail is an object whose code is FEATURE_NOT_AVAILABLE and whose feature is webhooks. | Move to a plan that carries them. Replaying the request changes nothing. |
| 422 | Validation refuses the body when: url is missing, url does not start with https://, url falls outside the bounds of 10 to 2048 characters, an event type is unknown, secret exceeds 128 characters, or an unknown field is present. detail is a list, and each entry carries loc, type and msg. | Read loc to know which value is refused, correct it, call again. |
| 429 | Your key exceeds its rate cap. detail starts with Rate limit exceeded, and X-RateLimit-Scope is key. | Wait the number of seconds given by Retry-After, then call again. |
| 429 | The sum of the keys of your brand exceeds the cap. detail starts with Brand rate limit exceeded, and X-RateLimit-Scope is brand. | Wait Retry-After. Creating one more key does not increase this cap. |
| 503 | The service that keeps the rate counters is momentarily unavailable. detail is "Rate limiting temporarily unavailable, please retry shortly". | We created nothing. Try again later. |
| 500 | A failure on our side. detail is "Internal Server Error", or "Internal server error" when the failure occurs while reading your key. | Try again. If the refusal persists, send us the X-Request-Id header of the response. |
#See also
GET /partner/webhooks, list your notification subscriptions, page by page.GET /partner/webhooks/{webhook_id}, read a subscription and its delivery state.PUT /partner/webhooks/{webhook_id}, modify the address, the events or the secret of a subscription.DELETE /partner/webhooks/{webhook_id}, delete a subscription and the signing secret attached to it.- Receiving events by webhook, create a subscription, verify a signature, catch up on the events that were lost.
Was this page helpful?
Your answer opens a pre-filled email in your mail app, addressed to contact@sealtrust.io. You read it over before sending it.