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:

HTTP
POST https://api.sealtrust.io/v1/partner/webhooks

The 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.

#Authorization

API key in the Authorization header, in the Bearer format followed by a space then the key.

What is requiredValue
AuthenticationAPI key of your brand
Scope carried by the keywebhooks:write
Feature of your planwebhook 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.

HeaderContent
X-RateLimit-Limitthe cap applied over the window
X-RateLimit-Remainingwhat is left to you in the current window
X-RateLimit-Resetthe end-of-window timestamp, in seconds
X-RateLimit-Scopekey 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.

NameTypeRequiredDescription
urlstringyesThe address that will receive the events. It must start with https:// and be between 10 and 2048 characters long.
eventsstring[]noThe event types you subscribe to. Each value must appear in the list below. Absent or empty, the subscription never receives anything.
secretstringnoThe shared secret that signs every delivery, 32 to 128 characters. Absent, we generate one and the creation response returns it to you, once.
brand_codestringnoThe 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.

HeaderContentCovered by the signature
X-Webhook-Eventthe event type delivered, for example product.scannedno
X-Webhook-Ida stable identifier for a given event, which you use to discard duplicates from resendsno
X-Webhook-Timestampthe timestamp used in the signature, in secondsyes

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"
  }'

#Example response

A successful creation answers 201 Created.

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"
}
FieldTypeWhat it contains
idintegerThe identifier of the subscription. It is the one you will pass to the read, modify and delete routes.
brand_codestringThe public code of the owning brand: the one of your key, or the client brand named.
brand_idintegerDeprecated: the brand's internal number, kept during the transition. Read brand_code.
urlstringThe address registered. It is this exact value that the deletion will ask you to copy back.
eventsstring[]The event types subscribed to.
is_activebooleantrue at creation. A modification can switch it to false to turn the subscription off without deleting it.
healthstringhealthy 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_atstringDate and time of creation, in ISO 8601 format.
updated_atstringDate and time of the last modification, in ISO 8601 format.
secretstringPresent 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.

CodeConditionWhat you must do
401The Authorization header is missing. detail is "Missing Authorization header", and the response carries WWW-Authenticate: Bearer.Add the Authorization: Bearer <votre clef> header.
401The 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.
401The value sent is empty or is shorter than 40 characters. detail is "Invalid API key format".Check that the key was copied in full.
401The 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.
403The key is no longer active. detail repeats its state, for example "API key is revoked".Use an active key.
403The 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.
403The key does not carry the required scope. detail is "Missing required scope: webhooks:write".Create a key that carries this scope.
403Your 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.
403brand_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.
422Validation 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.
429Your 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.
429The 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.
503The service that keeps the rate counters is momentarily unavailable. detail is "Rate limiting temporarily unavailable, please retry shortly".We created nothing. Try again later.
500A 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

Your answer opens a pre-filled email in your mail app, addressed to contact@sealtrust.io. You read it over before sending it.

Suggest a correctionReport a problem