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

Source: https://docs.sealtrust.io/en/reference/post-partner-sellout/

---

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.

## Authorization

API key in the `Authorization` header, in `Bearer` format, with the
`sellout:write` scope.

```http
Authorization: Bearer votre_clef
```

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

> [!ATTENTION] Three of these headers arrive in duplicate
> A second counter, the one of the address the call comes from, applies to our
> whole API and sets `X-RateLimit-Limit`, `X-RateLimit-Remaining` and
> `X-RateLimit-Reset` as well. The response therefore carries two values for
> each of those three headers, and most HTTP clients hand them back to you
> joined together and separated by a comma. Do not read any of the three as a
> number. `X-RateLimit-Scope` appears only once, and it is the only header that
> designates your key's or your brand's counter for certain.

## 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

:::onglets
```bash title="curl"
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"
  }'
```
```typescript
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());
```
```python
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())
```
:::

> [!INFO] The TypeScript SDK does not cover this endpoint yet
> The `@sealtrust-io/sdk` package exposes batch minting, verification and
> notification subscriptions. The sell-out declaration is therefore called in
> plain HTTP, as above.

## Example response

HTTP code 200, first declaration for this product.

```json
{
  "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.

```json
{
  "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}`](/en/reference/get-certificate/),
  read the certificate of authenticity of an item.
- [Getting started with the console](/en/console-prise-en-main/),
  the tour of the brand-side screens, in the order in which you use them.
- [Partner API, overview](/en/api-vue-ensemble/),
  base address, keys, scopes, rate limits and pagination.
- [API errors](/en/api-erreurs/),
  recognize an error code and decide whether to fix or replay.
