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, or, with a
reseller key, to the client brand named by brand_code. 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 subscription's brand is the key's. With a reseller key, brand_code names
one of your client brands. Another brand, or a code that names no brand, is
refused with 403 and nothing is created. It is the plan of the subscription's
brand that must include notifications. A subscription created for a client
brand is sent only while your reseller contract exists and the brand is still
your client, retries included.
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, 32 to 128 characters. Absent, we generate one and the creation response returns it to you, once. |
brand_code | string | no | The brand whose address receives the events. Absent, it is the key's brand. With a reseller key, the public code of one of your client brands, for example 7K3QXW9M2A. The old brand_id is still accepted in its place for now, but it is deprecated. Sending both is refused with 422. |
The body accepts only these three names. Any other field makes the request fail with a 422.
#The 17 accepted event types
They are exactly the types we deliver today. A name absent from this list makes the request fail with a 422.
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
#What is accepted is what is sent
Since 12 September 2026 the list of types accepted for a subscription is exactly
the list of types the product emits. Six names used to be accepted without
anything sending them: product.burned, product.status_changed,
batch.completed, batch.failed, warranty.expiring_soon and
certificate.issued. They are now refused at creation.
A subscription created before this change and carrying one of those names stays in place and receives nothing, as before.
#What the secret does
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.
A subscription always has a secret, and we refuse to send a delivery that
would not be signed. Without a secret in the request, we generate one of 43
characters.
#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_live_0000000000000000000000000000000000000000000000" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example-sas.example.com/sealtrust/evenements",
"events": ["product.scanned", "clone.alert"],
"secret": "secret-partage-de-demonstration-a-remplacer"
}'import { SealTrustClient } from "@sealtrust-io/sdk";
const sealtrust = new SealTrustClient({
apiKey: "st_live_0000000000000000000000000000000000000000000000",
});
const abonnement = await sealtrust.webhooks.create({
url: "https://example-sas.example.com/sealtrust/evenements",
events: ["product.scanned", "clone.alert"],
secret: "secret-partage-de-demonstration-a-remplacer",
});
console.log(abonnement.id, abonnement.health);import requests
response = requests.post(
"https://api.sealtrust.io/v1/partner/webhooks",
headers={
"Authorization": "Bearer st_live_0000000000000000000000000000000000000000000000",
"Content-Type": "application/json",
},
json={
"url": "https://example-sas.example.com/sealtrust/evenements",
"events": ["product.scanned", "clone.alert"],
"secret": "secret-partage-de-demonstration-a-remplacer",
},
timeout=30,
)
response.raise_for_status()
abonnement = response.json()
print(abonnement["id"], abonnement["health"])#Example response
A successful creation answers 201 Created.
{
"id": 41,
"brand_code": "7K3QXW9M2A",
"brand_id": 12,
"url": "https://example-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_code | string | The public code of the owning brand: the one of your key, or the client brand named. |
brand_id | integer | Deprecated: the brand's internal number, kept during the transition. Read brand_code. |
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. |
secret | string | Present only when the request carried no secret: the 43-character secret we generated. This is the only time it is readable. Absent when you supplied your own. |
#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. |
| 403 | brand_code names a brand the key does not serve, or names no brand. detail is "This API key cannot act for this brand.". | Fix brand_code with GET /v1/partner/catalog/brands. |
| 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 has fewer than 32 or more than 128 characters, brand_code does not have the shape of a ten-character code, brand_code and brand_id are sent together, 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.