Method POST/partner /sellout
Declare that a product has been handed over to the end customer, from a distributor's system. Requires the sellout:write scope.
On this page
You declare that a product has left the shelf and has been handed over to the end customer, naming the point of sale that sold it.
The full address is https://api.sealtrust.io/v1/partner/sellout. The same
route exists without the /v1 prefix, and the /v1 form is the one recommended
for a new integration.
This call can be replayed. A product that is already activated returns the
existing activation with status set to already_activated, and fills in only
the information that was missing: the attachment to the point of sale, the
country, the city. Nothing is overwritten. The Idempotency-Key header is not
read by this endpoint.
#
API key in the Authorization header, in Bearer format, with the
sellout:write scope.
Authorization: Bearer votre_clefA key without that scope receives a 403 whose message names the missing scope.
Your brand's plan must also include API access. Otherwise you receive a 403
whose detail field carries the code FEATURE_NOT_AVAILABLE.
#Rate limit
Two distinct limits apply.
| Limit | Value | What triggers it |
|---|---|---|
| Rate | 120 calls per 60 second window by default, the actual value depends on your plan | one counter per key and one counter for the sum of your brand's keys, with the same limit |
| Daily quota | the one attached to your key, empty means unlimited | 1 unit per call, deducted once the point of sale has been validated, before the product is resolved |
The rate counter starts again from zero at each new 60 second window. The daily quota starts again from zero at midnight universal time.
Every response that passes the rate limit carries the headers
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and
X-RateLimit-Scope. X-RateLimit-Reset gives the instant of the reset, in
seconds since January 1, 1970 in universal time. X-RateLimit-Scope equals
key or brand, and designates the more constraining of the two counters.
Creating extra keys does not increase the total rate allowed to your brand.
#Path and query parameters
This endpoint has no path or query parameter. Everything goes through the request body.
#Request body
Format application/json. Any field absent from this table causes the request
to be refused with 422.
| Name | Type | Required | Description |
|---|---|---|---|
identifier | string | yes | The product sold. From 1 to 200 characters. We accept four forms, see below. |
retailer_code | string | yes | The code of the point of sale, from 1 to 64 characters. It is the one you defined in your console, Distribution section. |
country | string | no | Country of the sale, two-letter ISO code. We uppercase the value. An empty string counts as absent. |
city | string | no | City of the sale, 100 characters maximum. |
#The four accepted forms for identifier
We try them in this order, and we always restrict the lookup to your brand.
| Form | Example | Recognized by |
|---|---|---|
| Label hash | 0x0000000000000000000000000000000000000000000000000000000000000000 | 0x followed by 64 hexadecimal characters, that is 66 characters in all. It is the value our responses return in the uid_hash field |
| Token identifier | 10000000000000000000000000000000000000000000000000000000000000000000000000000 | a string of digits, often very long. It is the value our responses return in the token_id field |
| Certificate number | ST-CERT-000000000000 | starts with ST-CERT-, followed by 12 characters. It is the number displayed on the certificate |
| Serial number printed on the product | 000000000000 | 12 characters of the Crockford Base32 alphabet |
The serial number tolerates the common reading confusions. We read the letters
I and L as the digit 1, the letter O as the digit 0, and we ignore
case. We never resolve a product that has been destroyed or withdrawn.
#What country and city are worth when you omit them
If you do not send country, the country recorded is the one of the point of
sale. Same rule for city. Fill in these two fields when the sale did not take
place at the usual address of the point of sale.
#Example request
curl -i -X POST https://api.sealtrust.io/v1/partner/sellout \
-H "Authorization: Bearer st_test_0000000000000000000000000000000000000000000000" \
-H "Content-Type: application/json" \
-d '{
"identifier": "000000000000",
"retailer_code": "BTQ-EXEMPLE-01",
"country": "FR",
"city": "Lyon"
}'const response = await fetch(
"https://api.sealtrust.io/v1/partner/sellout",
{
method: "POST",
headers: {
Authorization:
"Bearer st_test_0000000000000000000000000000000000000000000000",
"Content-Type": "application/json",
},
body: JSON.stringify({
identifier: "000000000000",
retailer_code: "BTQ-EXEMPLE-01",
country: "FR",
city: "Lyon",
}),
},
);
console.log(response.status);
console.log(response.headers.get("X-RateLimit-Scope"));
console.log(await response.json());import requests
response = requests.post(
"https://api.sealtrust.io/v1/partner/sellout",
headers={
"Authorization": "Bearer st_test_0000000000000000000000000000000000000000000000",
"Content-Type": "application/json",
},
json={
"identifier": "000000000000",
"retailer_code": "BTQ-EXEMPLE-01",
"country": "FR",
"city": "Lyon",
},
timeout=30,
)
print(response.status_code)
print(response.headers["X-RateLimit-Scope"])
print(response.json())#Example response
HTTP code 200, first declaration for this product.
{
"status": "activated",
"activation_id": 1024,
"product_id": 4096,
"product_name": "Sac de voyage Exemple SAS",
"retailer_code": "BTQ-EXEMPLE-01",
"source": "sellout_declared",
"activated_at": "2026-08-20T14:32:07.512430+00:00"
}HTTP code 200 as well if the product had already been activated, for example by a scan from the end customer before your declaration.
{
"status": "already_activated",
"activation_id": 1024,
"product_id": 4096,
"product_name": "Sac de voyage Exemple SAS",
"retailer_code": "BTQ-EXEMPLE-01",
"source": "qr",
"activated_at": "2026-08-19T09:14:55.201884+00:00"
}The seven fields of the response.
| Field | Type | Description |
|---|---|---|
status | string | activated if this call created the activation, already_activated if it already existed. |
activation_id | integer | Internal identifier of the activation. Use it to reconcile your records with ours. Do not display it to a third party. |
product_id | integer | Identifier of the product resolved from identifier. |
product_name | string or null | Name of the product as it is recorded. |
retailer_code | string | The point of sale code you sent, after resolution. On an already_activated response, if the activation already carried another point of sale, we keep that original attachment and this field does not reflect it. |
source | string | What created the activation: sellout_declared for a declaration through this route, nfc or qr for a first scan. |
activated_at | string or null | Date and time of the activation, in ISO 8601 format with time zone. For an activation that already existed, it is the original date, never that of your call. |
#Errors
| Code | Condition | What to do |
|---|---|---|
| 401 | Authorization header absent. The response carries WWW-Authenticate: Bearer. | Add the header Authorization: Bearer <your key>. |
| 401 | Header present but not starting with Bearer followed by a space. | Correct the format of the header. |
| 401 | Unknown key, or key shorter than 40 characters. | Check that you are sending the whole key. It is readable only once, when it is created; if it is lost, create a new one from the console. |
| 403 | Key revoked, or already marked as expired. The message names the state. | Create a new key. A revoked key is refused on every subsequent call, and the console offers no way to put it back in service. |
| 403 | Key that has reached its expiry date. Message API key has expired. | Create a new key. Expiry is observed on the first call after the deadline, and it is final. |
| 403 | The key does not have the sellout:write scope. Message Missing required scopes: sellout:write. | Create a key that carries that scope. The scopes of an existing key cannot be changed. |
| 403 | Your brand's plan does not include API access. detail equals {"code": "FEATURE_NOT_AVAILABLE", "feature": "api_access"}. | Contact us to change plan. Retrying will change nothing. |
| 404 | No active point of sale carries this retailer_code in your brand. Message Retailer '<code>' not found for this brand. | Check the code in your console, Distribution section, and check that the point of sale is active. |
| 404 | No product of your brand matches identifier. Message Product not found for this identifier. | Check the identifier and its form. A product of another brand, destroyed or withdrawn, answers the same thing. |
| 422 | Invalid body: required field absent, unknown field, country that is not two letters, or length exceeded. | The body of the response lists the offending fields and the reason for each refusal. Correct and call again. |
| 429 | Rate limit reached, by your key or by the sum of your brand's keys. | Wait the number of seconds given by the Retry-After header. X-RateLimit-Scope tells you which of the two counters refused. |
| 429 | Daily quota of the key exhausted. Message Quota exceeded. Remaining today: <restant>/<limite>. | The headers X-Quota-Limit and X-Quota-Remaining give the state of the counter. X-Quota-Reset gives the date of the last reset, a date in the past. The next one happens at midnight universal time. Wait for midnight universal time, or use a key with a wider quota. |
| 500 | Internal error while processing the call. | Retry. If the error persists, contact us, giving the time of the call and the retailer_code used. |
| 503 | The service that counts the calls is momentarily unavailable. Message Rate limiting temporarily unavailable, please retry shortly. | Retry in a few moments. No declaration has been recorded. |
#The order of the checks, and what it changes for your quota
The checks run in this order: rate limit, brand's plan, existence of the point of sale, daily quota of the key, then resolution of the product.
One useful consequence: an unknown retailer_code does not consume your daily
quota, whereas an identifier that cannot be found does consume it, because we
resolve the product after the deduction. If you reconcile sales in batches,
validate your point of sale codes once and for all, and expect unknown
identifiers to cost one unit each.
#See also
GET /certificate/{identifier}, read the certificate of authenticity of an item.- Getting started with the console, the tour of the brand-side screens, in the order in which you use them.
- Partner API, overview, base address, keys, scopes, rate limits and pagination.
- API errors, recognize an error code and decide whether to fix or replay.
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.