Changelog

What changed in the partner API and in notifications, by date, with the exact action to take when an entry breaks a call that used to work.

By the end of this page, you will know what changed in the partner API and in notifications, on what date, and what you have to do on your side when a change rejects a call that used to work. You will also know why the version number you call does not move while these entries pile up.

Read this page in full the first time. After that, come back to it before every change to your integration and stop at the first date you already know.

#How this page is built

Entries run from the most recent to the oldest. The date on an entry is the date the change went live.

An entry starts with Breaking change when a call that used to work is rejected afterward. That is the only case where you have work to do. The other entries describe behavior that is added or a response that becomes more precise, and your integration keeps working untouched.

Each entry says three things: what changes, the exact response you now receive, and the action to take. No entry sends you elsewhere for the action to take.

This page covers every endpoint this site documents: the key-authenticated partner API, the public endpoints that need no key, authenticated passport reads, the notifications sent to your addresses, and the partner portal. It does not cover the console, whose screens change without any contract to keep toward your code.

#Why the version number does not change

The GET /v1/version endpoint always answers the same thing.

JSON
{
  "version": "1.0.0",
  "api_version": "v1"
}

Every entry below fits inside version 1. The day a change cannot fit there, it will be added under /v2, and version 1 will keep its behavior.

The business endpoints exist at two addresses that call the same code: with the /v1 prefix, and with no prefix at all. Call GET /v1/version when you want the contract version. It is the only version endpoint this site documents.

Both addresses call the same code, so they receive every behavior change listed here at the same time. One entry is the exception, the one dated August 16, 2026, which concerned only the addresses without a prefix.

On this page
  1. #

    Three changes. The first is a breaking change, and it concerns every accredited partner.

    1. Breaking change

      #The value_chain access tier no longer exists

      The value_chain tier was granted to any partner holding any accreditation on the brand, and it returned the union of what the repairer, the recycler and the upstream supplier see. An accredited recycler could therefore read manufacturing data and the supply chain, which the recycler tier denies. That door is closed.

      Details

      Six tiers remain: public, end_user, repairer, recycler, upstream and authority. The three trade tiers are distinct audiences: holding one accreditation opens none of the others.

      The exact response. ?access_tier=value_chain now answers 422, on GET /v1/passport/{identifier}, on GET /v1/passport/01/{gtin} and on the three verifiable credential endpoints. detail is the usual validation list, each entry carrying loc, type and msg, and loc names access_tier.

      What you do. Ask for your own trade's tier, repairer or recycler. You will receive fewer fields than before, and they are precisely the fields that are not part of your trade.

    2. Change

      #The partner portal announces the trade it served

      In GET /v1/partner-portal/products/{identifier}, the passport.access_tier field always read value_chain. It now carries the trades actually served, joined by a + and sorted alphabetically: recycler, or repairer+recycler for an account that holds both accreditations on that brand. It reads public when the brand has recognized none of them, and the passport is then filtered at the public tier.

      Details

      What you do. If your code compares this value to value_chain, revisit the comparison. And expect narrower content than before: it is now filtered on your accreditations alone.

    3. Change

      #A unit's public image falls back to its model

      In GET /v1/products/{uid_hash}/public, the image_url field only looked at media attached directly to the unit, and read null as soon as it had none. It now follows the same fallback chain as the rest of the site: an image of the unit, then its model's main image, then the model's media.

      Details

      What you do. Nothing. A field that used to read null may now carry an address.

  2. #

    Three changes shipped together. None is a breaking change.

    1. Change

      #The public certificate names the issuing brand

      The issuer_name field carries the name of the brand that issued the certificate. The server computes it on read, and it reads null when the certificate is attached to no brand.

      Details

      Three surfaces are affected: GET /v1/certificate/{identifier}, the PDF returned by GET /v1/certificate/{identifier}/download, and the certificate block of GET /v1/resolve/{identifier}.

      What you do. Nothing in your calls. If you copied the value of issuer_name into your own database, replace it with what the API returns today.

    2. Change

      #Authentication refusals answer in English

      The API's front door returned its messages in two languages depending on the path called. The messages it puts in detail are now in English: Not authenticated, Invalid token, Invalid token: missing email, Token has been revoked, Invalid JWT token, Account disabled, User not found and Partner account required (repairer or recycler).

      Details

      Two refusals from POST /v1/originality/read-sig/verify follow the same rule and now read Invalid uid_hex and Invalid signature_hex.

      What you do. Nothing, if you key your code on the HTTP status code, as the errors page recommends. A client that compares these sentences character by character must revisit those comparisons, or better, remove them.

    3. Change

      #The partner portal has a rate limit

      The four /partner-portal endpoints accept 30 calls per 60 second window, counted per calling address. All four share that counter, and the /v1/partner-portal/... form shares it with the prefix-free form.

      Details

      Every accepted response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Going over returns 429, with detail reading Rate limit exceeded: 30 requests per 60s and a Retry-After header reading 60.

      What you do. Space out your calls to the portal and drive your pace from X-RateLimit-Remaining. A human journey, searching for a product then reporting an intervention, stays well below this limit.

  3. #

    1. Change

      #Sending a batch now creates products

      POST /v1/partner/mint/batch answered 200, its processing ran to completion, and no product was created. Every row was missing two values that the submission contract does not let you provide, and every row was rejected for that reason.

      Details

      The server now sets those two values itself, on every row: the identification method qr and the product's technical identifier.

      What you do. Nothing in your submissions. A row's contract does not change, and a row that carried the identification method or the technical identifier is rejected with 400, like any field the contract does not name.

      Products created this way are identified by QR. For NFC items, go through the console.

  4. #

    Seven changes shipped together. The first four are breaking changes.

    1. Breaking change

      #The API rejects the owner_email and contract_address fields

      A batch row that carries either of these two fields fails the whole request with 400. The response names the rejected field.

      Details

      The API used to accept these two fields and never read them. The value you put in them had no effect.

      A batch row now accepts five fields and nothing else: product_name, brand_id, category_id and metadata_uri, which are required, and external_ref, which is optional.

      What you do. Remove these two columns from your CSV exports and these two keys from your JSON objects. If you need to tie a row to a reference on your side, use external_ref: it is the contract's only free field. We keep it on the batch row and we return it in the result read by GET /v1/partner/mint/batch/status/{job_id}. No product field picks it up.

      Removing them makes validation pass. Since August 21, 2026, it also makes the products get created, see the entry for that date.

    2. Breaking change

      #Replaying an idempotency key with a different batch answers 409

      The Idempotency-Key header remains optional and is still remembered for 24 hours.

      Details

      Replaying the same value with the same batch still returns the first call's response, without minting anything again. The order of the checks changed the same day. A replay now consumes a token from your rate limit, and the API re-reads your request body before returning the cached response. A replay beyond the limit therefore receives 429. A replay whose body has become unreadable receives 400.

      Replaying the same value with a different batch now answers 409. The API does not queue your second batch, and the response tells you so.

      JSON
      {
        "detail": "Idempotency-Key 'lot-2026-08-20-a' was already used with a different request body"
      }

      The same 409 code also signals that a call carrying this key is still being processed. The message is then A request with this Idempotency-Key is already being processed. Read the message before deciding what to do: the first case asks you to change your key, the second asks you to wait for the processing under way to finish.

      Two batches are compared on their content once read. Row order, column order, the choice between CSV and JSON, and empty cells make no difference.

      What you do. Build one Idempotency-Key value per distinct batch. If your code reuses a fixed value, or a value derived from the time to the minute, change it now.

    3. Breaking change

      #Batch tracking requires the mint:batch scope

      GET /v1/partner/mint/batch/status/{job_id} requires the mint:batch scope, the same one as sending the batch. A key that does not carry it receives 403, and the message names the missing scope.

      Details

      What you do. Check that the key querying the tracking endpoint carries mint:batch. A key's scopes are set when it is created and no screen changes them afterward, so you need a new key.

      First check your key limit, which counts only active keys. The Essentiel plan allows only one: revoke the old key, then create the new one, accepting the outage between the two. The Prestige plan allows five: create the new key, deploy it, then revoke the old one. Revoking a key frees a slot immediately. A creation beyond the limit answers 403 with the QUOTA_EXCEEDED code.

    4. Breaking change

      #Changing a notification subscription requires your plan to include them

      PUT /v1/partner/webhooks/{webhook_id} now looks at your brand's plan. A call that worked the day before for a brand whose plan does not include notifications is rejected with 403.

      Details
      JSON
      {
        "detail": {
          "code": "FEATURE_NOT_AVAILABLE",
          "feature": "webhooks"
        }
      }

      Turning one off is the one exception: sending is_active at false and nothing else remains possible without the plan. As soon as another field accompanies that switch off, the API requires the plan.

      Creating a subscription, POST /v1/partner/webhooks, answers the same thing and already required it before this change.

      What you do. Nothing if your plan includes notifications. If it no longer includes them, stop changing your subscriptions: turn them off with is_active at false alone, or delete them.

    5. Change

      #An unreadable JSON body answers 400

      A malformed body sent to POST /v1/partner/mint/batch now answers 400, with a message that recalls the two accepted formats.

      Details
      JSON
      {
        "detail": "Corps de requete illisible : le JSON envoye est mal forme. Envoyez une liste d'objets produit en application/json, ou un fichier CSV en multipart/form-data."
      }

      What you do. If your code automatically replays responses in the 5xx family, this case no longer triggers it. A request rejected with 400 must be fixed, and replaying it as is will give the same refusal.

    6. Change

      #The eight key-authenticated endpoints are rate limited

      The rate limit now applies to all eight endpoints of the partner API, including batch tracking and the five notification subscription endpoints.

      Details

      Two visible consequences. Beyond the limit, you receive 429 with the Retry-After header, expressed in seconds. When the rate limiting service is unavailable, you receive 503, and the call has no effect: a deletion refused this way deletes nothing.

      Responses that go through carry X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and X-RateLimit-Scope.

      What you do. Read X-RateLimit-Remaining and slow down before you reach zero. If you poll a batch's tracking endpoint in a loop, space out your calls: it is the endpoint most exposed to this limit. Treat the 503 as a temporary outage and retry after a delay.

    7. Change

      #Your notification subscriptions stay readable and deletable whatever your plan

      Reading the list of your subscriptions, reading one subscription and deleting a subscription work whatever your brand's plan. These three calls do not look at your plan.

      Details

      Sending notifications follows another rule: a brand whose plan no longer includes notifications receives no more deliveries, whatever subscriptions are still recorded.

      What you do. Nothing. If your plan no longer includes notifications, you keep control to inventory your subscriptions and to delete them.

  5. #

    1. Change

      #Prefix-free addresses become permanent aliases

      The copies without the /v1 prefix used to answer with two headers announcing an end of service on September 1, 2026, Deprecation and Sunset. The API no longer sends these two headers. The /v1 addresses never carried them.

      Details

      No end-of-service date is attached to these addresses. If your integration already calls them, it keeps working.

      What you do. Nothing right now. Use the /v1 form for any new integration: it is the form documented everywhere on this site.

  6. #

    1. Change

      #A damaged gzip file answers 400

      POST /v1/partner/mint/batch accepts a CSV compressed with gzip or zlib.

      Details

      A gzip file the server cannot decompress now answers 400, with the reason in the message. It is a request error, and your code can treat it as such.

      A file whose decompressed content exceeds the limit answers 413, and the message states that limit in MiB.

      What you do. Nothing if your submissions are valid. If you compress on the fly, check that the stream is complete before sending it, and split your batches to stay under the limit. A batch carries at most 500 items.

    2. Change

      #Batch tracking returns four more fields when the queue has forgotten your batch

      GET /v1/partner/mint/batch/status/{job_id} used to answer unknown to the owner of an old batch, once the queue's retention period had elapsed. It now returns the real status, rebuilt from the reservation recorded when the batch was sent, and it adds four fields: batch_status, items_count, success_count and error_count.

      Details

      An identifier that designates no batch of your brand still answers unknown, revealing nothing more.

      What you do. Nothing, these fields are additions.

  7. #

    1. Change

      #A partner portal intervention carries a proof level

      POST /v1/partner-portal/interventions now records a proof level with every intervention, in the proof_level field.

      Details

      The request body accepts an optional proof_code field. Without it, the level reads declared. With a valid code, it reads customer_code when the code was read by the customer, or work_order when it comes from the brand's work order. A rejected code answers 422 and records nothing at all: neither the intervention, nor the consumption of the code.

      Interventions recorded before this date return proof_level empty. An empty value does not mean declared: the question had not been asked.

      What you do. If you display the intervention history, treat the empty value as a separate case and do not display "declared" in its place.

  8. #

    1. Breaking change

      #Deleting a subscription requires retyping its address

      DELETE /v1/partner/webhooks/{webhook_id} requires a confirm query parameter containing the subscription's exact address.

      Details

      Without that parameter, a subscription that belongs to you answers 400 with the CONFIRMATION_REQUIRED code. With a value that does not match, the response is 400 with the CONFIRMATION_MISMATCH code, and the API changes nothing. A successful deletion answers 204.

      An identifier that designates no subscription of your brand answers 404, whether you sent confirm or not: the API looks for your subscription before looking at your confirmation.

      A call that sent only the identifier is therefore rejected.

      What you do. Read the address with GET /v1/partner/webhooks/{webhook_id}, then send it back in confirm.

      curl
      curl -X DELETE \
        "https://api.sealtrust.io/v1/partner/webhooks/1?confirm=https%3A%2F%2Fexemple-sas.example%2Fwebhooks%2Fsealtrust" \
        -H "Authorization: Bearer st_test_0000000000000000000000000000000000000000000000"
    2. Change

      #Every delivery carries the event type

      Every delivery now carries X-Webhook-Event, which names the event type. The signature does not cover this header.

      Details

      What you do. Nothing, this header is an addition.

  9. #

    1. Change

      #Two headers are added to every delivery

      Every delivery now carries X-Webhook-Id and X-Webhook-Timestamp, in addition to X-Webhook-Signature.

      Details

      X-Webhook-Id is the hash of the delivery body. Retries of the same delivery carry the same value, which lets you discard a retry. Two distinct events whose body is identical also carry the same value.

      What you do. Deduplicate on the pair formed by the event name, carried by X-Webhook-Event since August 11, 2026, and X-Webhook-Id. Never discard a delivery on the value of X-Webhook-Timestamp: that timestamp changes with every retry, so it would make you process the same delivery twice.

    2. Change

      #The signature covers the exact bytes sent to you

      X-Webhook-Signature has the form t=<timestamp>,v1=<hash>. The hash is an HMAC-SHA256 computed with your subscription's secret, over the string formed by the timestamp, a period, and the request body.

      Details

      The signed body is the byte sequence received on the wire. Decoding the JSON then re-encoding it before verifying breaks the signature, because re-encoding does not produce the same bytes.

      What you do. Verify the signature on the raw request body, before any decoding.

  10. #

    1. Change

      #Subscription endpoints require scopes

      Listing your subscriptions and reading one require the webhooks:read scope. Creating, changing and deleting a subscription require the webhooks:write scope.

      Details

      A key that does not carry the scope receives 403, and the message names the missing scope.

      What you do. Check the scopes of the key that manages your subscriptions. If it was created with the mint:batch scope alone, create a new key that also carries the notification scopes.

  11. #

    1. Change

      #The /v1 prefix appears

      Every business endpoint becomes reachable at two addresses, with the /v1 prefix and with no prefix at all.

      Details

      The prefix-free addresses additionally answer two headers, Deprecation: true and Sunset: 2026-09-01. The /v1 addresses do not carry them. The August 16, 2026 entry removes these two headers and no end-of-service date was ever applied.

      What you do. Nothing. Both addresses call the same code.

#What this page does not do

It does not announce upcoming changes. An entry lands here when the behavior described is live.

It does not replace a test. We open no test environment to partners, and no sandbox address exists. After reading an entry marked as a breaking change, replay your real call on a product or a subscription of your brand that you can sacrifice, and compare the response code to the one you expected.

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