# Partner API, overview

Base address, key authentication, permissions, call limits, replaying a call without doubling its effect, pagination and versioning. The page to read before opening the reference.

Source: https://docs.sealtrust.io/en/api-vue-ensemble/

---

By the time you leave this page, you will know how to authenticate a call to
the partner API, choose the right address, read the headers that tell you how
many calls you have left, replay a request without doubling its effect, and
recognize the three limits that can refuse you. This is the page to read once,
before opening the reference endpoint by endpoint.

## What this API does

The key-authenticated partner API is there to drive SealTrust from your own
system, without going through the console. It has eight endpoints, split across
three uses.

| Use | Endpoints |
| --- | --- |
| Create products in batches, and track the progress of the processing | 2 |
| Report a sale to the end customer | 1 |
| Manage your notification subscriptions | 5 |

Submitting a batch only creates products identified by QR. The server itself
sets, on each line of the batch, the identification method and the technical
identifier of the item. Your lines carry neither of them, and you cannot ask
for any others.

> [!ATTENTION] `finished` describes the execution queue, never the outcome
> `POST /partner/mint/batch` replies `200` as soon as we accept the batch,
> before any processing. The status call then replies `status: "finished"` as
> soon as the execution queue has finished processing the batch, whatever the
> fate of each line. A batch whose lines were all rejected replies `finished`
> too. So do not wire your failure detection to `status: "failed"`. When the
> execution queue no longer keeps the batch, the response is built from your
> reservation and carries four more fields: `batch_status`, `items_count`,
> `success_count` and `error_count`. A wholly rejected batch then settles with
> `status: "failed"`, `success_count: 0` and an `error_count` equal to the
> number of lines submitted.

There is no endpoint for listing your products, nor for reading a single one
with an API key. You read a product through the public verification endpoints,
which require no key.

The partner portal, at the address `/partner-portal`, is a different surface.
It authenticates with the session of a repairer or recycler partner account,
and an API key gives no access to it.

## The base address and the /v1 prefix

You call the API at `https://api.sealtrust.io`.

Every endpoint described on this site exists at two addresses that call exactly
the same code: with the `/v1` prefix, and with no prefix at all.

```http
POST https://api.sealtrust.io/v1/partner/mint/batch
POST https://api.sealtrust.io/partner/mint/batch
```

Use the `/v1` form for any new integration.

The reference pages on this site title each endpoint with its path without a
prefix, for example `POST /partner/mint/batch`. Add `/v1` in front of that path
when you write your call.

The addresses without a prefix are permanent aliases. We attach no
end-of-service date to them, and we send no removal announcement header on
those responses. If your integration already calls them, it will keep
working.

> [!INFO] What a possible version 2 will be
> The day a breaking change arrives, it will be added under `/v2`. The root
> will stay frozen on the behavior of version 1. An installed integration does
> not update itself, and that is what our commitment is about.

## Authenticating a call

You send your key in the `Authorization` header, in `Bearer` format.

```http
Authorization: Bearer votre_clef
```

That is the only authentication mode of this API. There is no URL parameter, no
cookie, and no request signature to compute.

### Getting a key

You create your keys from your brand console, under **Settings** then the
**Developers** tab. The tab stays visible whatever your plan. If your plan does
not include API access, the screen is shown locked and you cannot create any
key there.

Three points matter at creation time.

- **The full secret is displayed only once**, in the window that follows
  creation. Copy it at that moment. No screen and no endpoint lets you read it
  again afterwards. We keep only a fingerprint of it.
- **A key belongs to a single brand.** We attach every operation made with it
  to that brand, and to no other.
- **A key always expires.** You choose a lifetime at creation. If you do not
  choose one, it is 365 days. Note the date in your calendar: on the day, your
  calls stop.

The console then displays the first characters of each key, so that you can
recognize it in the list without ever showing its secret again. It also
displays the date of last use. We refresh that date at most once a minute, so
it can lag one minute behind your last call.

### Revoking a key

You revoke a key from the same page, and the revocation takes effect
immediately. The first call that follows is refused. Revoking a key frees a slot if
your plan limits the number of active keys.

> [!DANGER] A compromised key is revoked
> There is no way to rotate the secret of an existing key. If a secret may have
> leaked, revoke the key and create a new one, then replace the value in your
> system.

### Authentication refusals

| Code | What happened | What to do |
| --- | --- | --- |
| 401 | the `Authorization` header is missing | add it, the response also carries `WWW-Authenticate: Bearer` |
| 401 | the header does not start with `Bearer ` followed by a space | correct the form of the header |
| 401 | the key is malformed or unknown | send the full secret back exactly as the console displayed it, with no space and no line break |
| 403 | the key has been revoked | create a new key in the console |
| 403 | the key has reached its expiry date | create a new key, the old one will never become valid again |
| 403 | the key does not have the permission required by this endpoint | the message names the missing permission, see [the permissions attached to a key](#the-permissions-attached-to-a-key) |

## Your first call

Check that a key works with the list of your notification subscriptions. This
endpoint changes nothing and consumes no quota. It requires the `webhooks:read` permission.

:::onglets
```bash title="curl"
curl -i https://api.sealtrust.io/v1/partner/webhooks \
  -H "Authorization: Bearer st_test_0000000000000000000000000000000000000000000000"
```
```typescript
import { SealTrustClient } from "@sealtrust-io/sdk";

const client = new SealTrustClient({
  apiKey: "st_test_0000000000000000000000000000000000000000000000",
});

const page = await client.webhooks.list();

console.log(page.total);
console.log(page.items);
```
```python
import requests

response = requests.get(
    "https://api.sealtrust.io/v1/partner/webhooks",
    headers={
        "Authorization": "Bearer st_test_0000000000000000000000000000000000000000000000"
    },
    timeout=30,
)

print(response.status_code)
print(response.json())
```
:::

Response, HTTP code 200, for a brand that has no subscription yet:

```json
{
  "items": [],
  "total": 0
}
```

The TypeScript SDK gives you the body of the response, never its headers. To
read the limit headers described in
[The rate limit, in calls per minute](#the-rate-limit-in-calls-per-minute), call the API over direct HTTP
from your server, or go through `curl -i`. A browser cannot read them: the only
header we expose to the browser is `X-Total-Count`.

## The permissions attached to a key

Every key carries a list of permissions. A missing permission gets the call
refused with a 403, and the refusal message names the permission that is
missing.

Four permissions govern access to the endpoints of this API.

| Permission | What it opens |
| --- | --- |
| `mint:batch` | submit a batch of products, and read the progress of a batch |
| `sellout:write` | report a sale to the end customer |
| `webhooks:read` | list your subscriptions, and read a single one |
| `webhooks:write` | create, modify and delete a subscription |

> [!ATTENTION] Only tick what you need
> The creation form offers other boxes. Today they open none of the eight
> endpoints described on this site. Tick the permissions in the table above,
> and only those your integration actually uses.

## The three limits

Three different counters can refuse a call. They are not alike and they are not
read in the same place. Treat them separately.

### The rate limit, in calls per minute

We count your calls over a fixed 60-second window. This limit applies to all
eight endpoints, including reading the status of a batch and deleting a
subscription.

Two counters run at the same time: one for your key, one for the sum of all the
keys of your brand. The limit is the same on both sides.

> [!INFO] Adding keys does not buy rate
> The brand counter bounds the total. Creating a second key to double your pace
> does not work: both keys feed the same brand counter.

Your limit comes from a value set on your account, or failing that from your
plan. When neither of the two is defined, you get 120 calls per window. Do not
guess this value: you read it on every response.

Every response that passes the limit carries the following four headers.

| Header | What it contains |
| --- | --- |
| `X-RateLimit-Limit` | the applicable limit, in calls per window |
| `X-RateLimit-Remaining` | what is left to you in the current window |
| `X-RateLimit-Reset` | the timestamp, in seconds since 1970, at which the window restarts |
| `X-RateLimit-Scope` | `key` or `brand`, depending on which of the two counters is the more constraining |

> [!ATTENTION] Three of these headers arrive in two copies
> A second counter, the one for the address the call comes from, applies to our
> whole API and also sets `X-RateLimit-Limit`, `X-RateLimit-Remaining` and
> `X-RateLimit-Reset`. The response therefore carries two values for each of
> these three headers, and most HTTP clients give them back to you stuck
> 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 names
> for certain the counter of your key or of your brand.

These headers always describe the tighter of the two counters. Drive your pace
from them, and slow down before reaching zero.

Going over returns a 429 with the same headers, plus `Retry-After`.
`Retry-After` counts the seconds left in the current window, and is at least 1.
Respect it. `X-RateLimit-Scope` tells you which of the two counters refused,
which saves you from looking on the key side when it is the brand total that is
full.

One last case, a rare one: if the service that keeps these counters is
momentarily unavailable, all eight endpoints reply 503. A 503 means that the
call did nothing at all. Try again later.

### The daily quota of the key

Every key can carry a daily quota. A key without a quota is unlimited on that
side. The counter restarts from zero when midnight passes in universal time.
Your time zone does not come into it.

This quota is counted per line submitted.

- A batch of 100 lines consumes 100 units.
- A sale report consumes 1 unit.
- The six other endpoints consume nothing: the five notification subscription
  endpoints, and reading the progress of a batch.

Going over returns a 429 with three headers of its own.

| Header | What it contains |
| --- | --- |
| `X-Quota-Limit` | the daily quota of the key |
| `X-Quota-Remaining` | what is left for today |
| `X-Quota-Reset` | the date of the last reset of the counter |

The response also carries the four `X-RateLimit-*` headers of the rate check,
which the call had just passed before being stopped by the quota.

> [!ATTENTION] Two different limits return 429
> A 429 on its own does not say which of the two counters spoke. Look at two
> headers, and two only. `Retry-After` appears only on a rate refusal, which is
> made up in a few seconds. `X-Quota-Limit` appears only on a daily quota
> refusal, which is made up only at the next universal midnight. The
> `X-RateLimit-*` headers accompany both refusals, they distinguish nothing.

### The monthly quota of your plan

Your plan sets a number of products that can be created per month, over your
billing period. It also decides whether API access is open to you, and whether
notification subscriptions are open to you.

Three 403 refusals follow from it, all carrying a code readable by your
program.

Your plan does not include API access:

```json
{
  "detail": {
    "code": "FEATURE_NOT_AVAILABLE",
    "feature": "api_access"
  }
}
```

Your plan does not include notification subscriptions:

```json
{
  "detail": {
    "code": "FEATURE_NOT_AVAILABLE",
    "feature": "webhooks"
  }
}
```

That refusal only touches the creation and the modification of a subscription.
You keep reading and deletion whatever your plan. You also keep switching a
subscription off, on one condition: send `is_active` to false and nothing else.
As soon as a second field accompanies that value, the call counts as a
modification and the refusal applies.

You have reached the number of products of your billing period:

```json
{
  "detail": {
    "code": "QUOTA_EXCEEDED",
    "resource": "products",
    "current": 4800,
    "additional": 300,
    "max": 5000,
    "period": "monthly"
  }
}
```

This one reads as follows: you have already created 4800 products in the
period, you are asking for 300 more, the maximum is 5000. Split your batch or
wait for the next period.

## Replaying a call without doubling its effect

Submitting a batch accepts an `Idempotency-Key` header. This header carries an
idempotency key, that is, a value that guarantees that one and the same
submission sent twice produces only one processing run. You choose its value,
and you keep it for the time of your attempts.

```http
Idempotency-Key: lot-exemple-0001
```

This header answers the case where you get no response: network cut, timeout,
restart on your side. You do not know whether the batch went out. Send the same
request again with the same value, and you get the response of the first call
without a second batch going into processing.

Three behaviors to know.

- **Same idempotency key, same batch**: you get back the response of the first
  call. We do not start a second processing run.
- **Same idempotency key, different batch**: the response is 409. We do not
  give you the response of the first call, because it does not describe what
  you have just sent.
- **Same idempotency key while the first call is still being processed**: the
  response is 409 as well. Wait for the first call to finish, then try again.

We keep a value for 24 hours. After that delay, the same value becomes a fresh
request again, and sending it again would put a second batch into processing.
So never use a fixed value: draw a new value per batch, and keep it for the
time of the attempts for that batch.

The comparison between two batches is made on their content once normalized.
The order of the lines, the order of the columns, the format chosen between CSV
and JSON and the cells left empty change nothing: we recognize two submissions
of the same content as identical.

A resend that returns the cached response still consumes one call against your
rate limit. It consumes neither your daily quota nor the monthly quota of your
plan.

> [!ATTENTION] Idempotency covers only one endpoint
> Only the batch submission reads this header. The report of a sale to the end
> customer does not read it. It does not create a duplicate for all that: it
> replies `already_activated` if the product had already been reported. Each
> resend consumes one unit of your daily quota nonetheless.

## Pagination

Only one endpoint of this API returns a list: `GET /v1/partner/webhooks`.

| Parameter | Type | Default value | Description |
| --- | --- | --- | --- |
| `skip` | `integer` | `0` | number of elements to skip, starting from 0 |
| `limit` | `integer` | `20` | number of elements to return, between 1 and 100 |

The response contains two fields: `items`, the requested page, and `total`, the
total number of subscriptions of your brand. So you walk the list by increasing
`skip` by the value of `limit` until you cover `total`.

> [!INFO] The response does not give your pagination back to you
> The response repeats neither `skip` nor `limit`. Keep them on your side
> during the walk.

You receive the subscriptions from the most recent to the oldest.

## What to remember before opening the reference

- The address is `https://api.sealtrust.io`, and the form to use is `/v1`.
- The key goes in `Authorization: Bearer`, and nowhere else.
- The secret is displayed only once. A key always expires.
- A 401 speaks about the key itself. A 403 speaks about a permission, a status,
  or what your plan allows.
- A 429 comes either from the rate or from the daily quota. `Retry-After`
  points to the rate, `X-Quota-Limit` points to the daily quota.
- A 503 comes from the service that keeps the rate counters, and means that the
  call did nothing.
- The status of a batch replies `finished` when the execution queue has
  finished, never to say that the lines succeeded.
- A batch request sent again after a cut must carry the same
  `Idempotency-Key` as the first attempt.

Every endpoint has its own page, with its parameters, its real response and its
full table of errors. You find them in the sidebar, arranged by task: "Create
products", "Report a sale", "Receive events".

- [POST /partner/mint/batch](/en/reference/post-partner-mint-batch/)
- [GET /partner/mint/batch/status/{job_id}](/en/reference/get-partner-mint-batch-status/)
- [POST /partner/sellout](/en/reference/post-partner-sellout/)
- [POST /partner/webhooks](/en/reference/post-partner-webhooks/)
- [GET /partner/webhooks](/en/reference/get-partner-webhooks/)
- [GET /partner/webhooks/{webhook_id}](/en/reference/get-partner-webhooks-id/)
- [PUT /partner/webhooks/{webhook_id}](/en/reference/put-partner-webhooks-id/)
- [DELETE /partner/webhooks/{webhook_id}](/en/reference/delete-partner-webhooks-id/)

The [Errors](/en/api-erreurs/) page gathers the codes common to the whole API.
