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

    1. Change

      #A piece keeps its identity when its brand changes offer

      A piece created on the Compliance + Identity offer keeps its identity functions when its brand moves to the Compliance offer: the claim with its purchase code, finishing a claim already started, transfers (starting, accepting, declining them), reading its NFC chip, the repair request and the owner's space. Before, these calls answered 403 OFFER_EXCLUDES_IDENTITY for every piece of a brand on the Compliance offer.

      Details

      The answer you receive now. These calls answer as on the Compliance + Identity offer. GET /timeline/{identifier} and GET /my-products carry identity_included set to true for these pieces. Creating a piece, ordering chip encoding and issuing a new purchase code still answer 403 OFFER_EXCLUDES_IDENTITY on the Compliance offer.

      What you do. Nothing. If your integration hid the claim or the transfer based on the brand's offer, read the piece's identity_included instead.

    2. Change

      #The admin MCP server is added to Claude with your account

      The admin server, which prepares a brand's catalog without ever publishing, is served at https://mcp.sealtrust.io/admin/mcp. You add it as a custom connector in Claude, sign in to the SealTrust console, choose a brand and what the assistant may do. No key to copy. The program run on your machine with a key stays available and does not change.

      Details

      The response you receive now. Without a token, POST /admin/mcp answers 401 with a WWW-Authenticate header that gives the address of the resource metadata. Claude follows that address, registers, then sends you to the "Connect an AI assistant" page of the console. A token is valid 30 minutes, a connection 90 days at most. Beyond 60 tool calls per minute per connection, the answer is 429 with a Retry-After header. A SealTrust team account cannot be connected. "Prepare drafts" is never ticked in advance, and changing or resetting your password ends your connections. The detail is in Connect the MCP server.

      What you do. Nothing for an existing integration. To end a connection, open Settings, then Developers, "AI assistant connections" card.

  2. #

    1. Breaking change

      #every notification is signed, the secret is mandatory

      A notification subscription now always has a signing secret, and we refuse to send a delivery that would not be signed. On POST /v1/partner/webhooks, a supplied secret is 32 to 128 characters long. Without a secret, we generate one of 43 characters. On PUT /v1/partner/webhooks/{webhook_id}, the secret can no longer be erased: null, an empty string or a secret under 32 characters is refused.

      Details

      The response you receive now. A 422 for a secret that is too short or empty, and for a null secret in a modification, with loc naming secret. A creation without a secret answers 201 with one more field, secret: this is the only time it is readable. A creation with your own secret does not return it, as before.

      What you do. If your secret is under 32 characters, take a longer one. If you create without a secret, read the secret field of the response and configure it on your receiver. To keep the current secret in a modification, omit the field instead of sending null.

    2. Change

      #A retry follows the subscription as it is when it leaves

      A retry reads the subscription again just before it is sent. It goes to the current address, signed with the current secret. It is dropped without being sent when the subscription was deleted, turned off, unsubscribed from the event, or when your plan no longer includes notifications. Before, a retry already scheduled went to the old address with the old secret.

      Details

      The response you receive now. After a secret change, the retries waiting their turn arrive signed with the new secret.

      What you do. Nothing, if your receiver accepts both secrets during a switch, as this documentation advises.

    3. Change

      #The MCP server is added to Claude and ChatGPT with an address

      The MCP server is served at https://mcp.sealtrust.io/mcp, without an account and without a key, in addition to the npm package. It carries the same seven tools, with the same descriptions, and is pasted as is as a custom connector in Claude or in developer mode in ChatGPT.

      Details

      The response you receive now. The transport is the Streamable HTTP of the MCP specification, without a session: each message is an independent POST, and a GET or a DELETE answers 405. A request carrying an Origin header from a site other than Claude or ChatGPT answers 403. Beyond 300 requests per minute per address, or 120 tool calls per minute per client, the answer is 429 with a Retry-After header. The detail is in Claude and ChatGPT in 30 seconds.

      What you do. Nothing if you use the npm package: it does not change. For an online assistant, paste the address instead of installing anything.

    4. Change

      #The catalog by key reads batches back, lists passports, previews and gives templates

      Five endpoints are added to the catalog API, all with the catalog:read scope:

      Details
      • GET /partner/catalog/brands/{brand_code}/batches, one batch with ?batch_code= or the list of batches;
      • GET /partner/catalog/brands/{brand_code}/passports/list, the latest version of each model and batch passport, without its content;
      • POST /partner/catalog/brands/{brand_code}/passports/preview, what each audience will see of a draft, writing nothing;
      • GET /partner/catalog/brands/{brand_code}/templates and GET /partner/catalog/brands/{brand_code}/templates/{template_key}, the passport templates.

      The draft passport write accepts one more field, only_if_draft. Set to true, it only completes the open draft: a sealed or published latest version is refused with a 409, and no version at all with a 404.

      The answer you receive now. The same answers as before on the existing endpoints. Without only_if_draft, a push on a sealed version still opens the next version.

      What you do. Nothing. The detail is on the page Fill the catalog through the API.

  3. #

    1. Change

      #The owner's product list shows the model's photo

      GET /my-products answered media: [] for a piece minted from a model when that model's photo exists only as its cover image, with no media attached. The public page of the same product showed that photo all the same.

      Details

      The response you receive now. When a piece's media list holds no image, the model's cover image is added to it first, with the identifier 0, media_type set to image and the public address of the photo. A piece that has its own photo keeps exactly its list.

      What you do. Nothing. If you took the first image of media as the thumbnail, it now appears. Do not look for the entry with identifier 0 in the brand's media library: it has no record of its own.

    2. Change

      #Claiming by serial number a piece you already own

      POST /claims/from-serial answered 409 "Already owned" or "Already claimed" to the owner himself, as to a stranger.

      Details

      The response you receive now. When the calling account is the recorded holder of the piece, the response is 409 with detail "ALREADY_YOURS: you already own this product". Any other account receives exactly the response it received before: the piece has an owner, never which one.

      What you do. Read the code before the colon. On ALREADY_YOURS, tell the person the product is already in their space, without sending them to the brand.

    3. Change

      #The signed credential of a model or a lot has its address

      The signed credential of a model passport is read at GET /passport/01/{gtin}/vc, and the one of a lot passport at GET /passport/01/{gtin}/10/{lot}/vc. Each one also has its /vc/verify form and its /vc/preview form, with the same access tiers as the address of an item. Until now these credentials were signed at publication but could only be read through one of the items of the model: those of a brand on the Compliance offer, which registers no item, could not be read.

      Details

      The response you receive now. These addresses answered 404. They now answer like GET /passport/{identifier}/vc and its two neighbours, with level (model or batch), gtin and, for a lot, batch_code in addition.

      What you do. Nothing. To verify the passport of a model or a lot, call the new address.

  4. #

    1. Change

      #The size, colour and weight of a passport are public

      The size, color and weight fields written at the root of a passport are now returned at the public tier, on GET /passport/{identifier}, in the public copy and in the signed credential. They were reserved to authorities. A brand that has its own access rules keeps its choice.

      Details

      The response you receive now. These three fields, when the passport carries them, in the public response.

      What you do. Nothing.

    2. Change

      #The signed credential discloses what the page shows

      GET /passport/{identifier}/vc and its per-tier presentation now follow exactly the rules of GET /passport/{identifier}: the product group is read from the passport, so the label mentions of a textile (fibre composition) and of a shoe are public in it, and Annex XIII of a battery opens and closes the same fields in it. A list is disclosed row by row and field by field, as the page cuts it.

      Details

      The response you receive now. At the public tier, a textile's credential carries composition with the material and percentage of each row. It did not. A battery's credential no longer carries the part numbers, the battery status or the expected lifetime in cycles.

      What you do. Nothing. A credential already issued keeps its disclosures; it is issued again at the passport's next publication.

    3. Breaking change

      #a brand number is written with digits only

      A brand number (brand_id, parent_brand_id) is now read one way everywhere: a JSON integer, or a string made of digits only. A brand code (brand_code) is a public code of ten characters or, during the transition, digits. Before, the API converted other spellings without saying so: 5.0, "+5", " 5", "5.0", "0_5" and true, the last one read as brand 1. The rule holds for the path, the query parameters, the body and each row of a batch, on every endpoint.

      Details

      The response you receive now. A badly written value gets a 422 whose detail.code is BRAND_REF_NOT_WELL_FORMED. detail.references says where the value is, without repeating it. In a batch sent as CSV, the faulty row is reported in the usual 400 refusal, and its message starts with BRAND_REF_NOT_WELL_FORMED. The whole batch is refused and nothing is created.

      What you do. Send the number with digits only, 5 or "5", or better, the brand_code that GET /v1/partner/catalog/brands gives. If your code produces a decimal number, convert it to an integer before sending.

    4. Change

      #Purchase codes are issued in bulk through the API

      A key carrying the new claim_codes:issue scope issues the purchase codes of the "purchase code required" rule for a list of pieces, 500 at most per call: POST /partner/claim-codes/brands/{brand_code}/issue. It reads their status, never the code, with POST /partner/claim-codes/brands/{brand_code}/status. POST /partner/mint/batch?issue_purchase_codes=true asks for a code per piece at mint time, and GET /partner/mint/batch/{job_id}/items gives them once, in purchase_code.

      Details

      The response you now receive. Each code is shown once. A piece whose code was already used gets a 409 PURCHASE_CODE_ALREADY_USED, unless you send "replace_used": true. A piece of another brand gets the same 404 PIECE_NOT_FOUND as an unknown piece. The response of POST /partner/mint/batch carries a sixth field, issue_purchase_codes.

      What you do. Nothing if you do not use purchase codes. Otherwise, tick claim_codes:issue on a dedicated key, never ticked by default, and read Issue purchase codes.

    5. Change

      #Verifying a signed QR code no longer says "authentic"

      GET /qr/verify no longer answers "Authentic product: QR code and blockchain verified.". A signed QR code proves that the brand registered this number and that the chain confirms it, not that the object in hand is the original: a QR code can be photocopied. The refusal no longer says "Invalid product": it is the code that does not match, nothing is known about the product itself.

      Details

      The response you receive now. The same valid field. message is "Registered number: the QR signature and the on-chain record match. A QR code can be copied: it does not prove the item in hand is the original." or "Unrecognised code: the QR signature or the on-chain record does not match.".

      What you do. Nothing if you read valid, as this page asks. If you displayed message, never display "authentic" from this response.

    6. Change

      #The public read of a product gives its registration date and its certificate state

      GET /products/{uid}/public returns two more fields. registered_at carries the date the product's record was created, under a name that says what it is. certificate_status carries the state of the certificate of authenticity in force, read exactly as GET /resolve/{identifier} reads it: active, or null with no certificate in force. verified_at stays, with the same value as registered_at.

      Details

      The response you receive now. Sixteen fields instead of fourteen. Nothing else changes.

      What you do. Nothing is required. If you displayed verified_at as a verification date, read registered_at and write "registered on": nobody verified the object on that date.

    7. Change

      #The PDF certificate no longer says "authentic, blockchain verified"

      The watermark seal of GET /certificate/{identifier}/download now reads REGISTERED and ON A PUBLIC BLOCKCHAIN (in French ENREGISTRÉ and SUR UNE BLOCKCHAIN PUBLIQUE). It read AUTHENTIC and BLOCKCHAIN VERIFIED. A public chain proves a registration was made, not that the object in hand is the original. The default footer follows the same rule. A revoked or expired certificate no longer has a seal: it carried the same check mark as a valid one.

      Details

      The response you receive now. A PDF whose seal and footer have new text. The rest of the document is unchanged.

      What you do. Nothing, unless you compare the PDF's text with an expected value.

  5. #

    1. Change

      #A brand is named by its public code in the partner API

      Every brand has a public code of ten characters, brand_code, for example 7K3QXW9M2A. The partner API now names the brand by this code: in the path of the /partner/catalog/brands/{brand_code}/... routes, in the body of POST /partner/sellout and of POST /partner/webhooks, in the list GET /partner/webhooks?brand_code=, and on every row of POST /partner/mint/batch. GET /partner/catalog/brands gives the code of every brand your key may act for. The public routes GET /brands/{brand_code}/branding and GET /brand/{brand_code}/pubkey also accept the code.

      Details

      The response you now receive. The answers of the partner API, of the partner portal, of /sdm/verify and of the public keys that returned brand_id also return brand_code. brand_id is deprecated there. A code that names no brand gets the same 403 as a brand the key does not serve. A request that sends brand_code and brand_id together is refused: with 422 for a sale or a subscription, with 400 for a batch row.

      What you do. Nothing right away: the brand_id number is still accepted everywhere it was. Replace it with brand_code at your next change. GET /brand/{brand_id}/did.json keeps the number, because it is part of the did:web identifier already signed into the issued credentials.

    2. Change

      #The address to paste into your ERP names the brand by its public code

      Shopify, WooCommerce, HubSpot and Akeneo send their events to the address /webhooks/integrations/{provider}/{brand_code}. The console, in Settings > Integrations, now shows this address with the brand's public code, on each connector's card and in the creation wizard.

      Details

      The response you now receive. An address whose code names no brand gets the same 404 as a brand without an active connector. The address that carries the brand's number keeps working.

      What you do. Nothing: an address already pasted into your ERP keeps working. For a new connector, paste the address the console shows.

    3. Change

      #The owner's space gives the brand's public code

      The answers of the owner's space that returned brand_id also return brand_code: GET /my-products, its history, the brand list GET /my-products/products/brands, the /ownership/... routes, the buybacks, the returns, the messages and the handover codes /custody/repair-codes. The GET /my-products/export/csv export gains a brand_code column, placed right after brand_id.

      Details

      The response you now receive. brand_id is deprecated there. GET /my-products accepts the ?brand_code= filter.

      What you do. Nothing right away: brand_id is still returned. Read brand_code at your next change. If you read the export by column position, shift every column that follows brand_id by one.

    4. Change

      #A textile's composition and a shoe's materials are public

      data at the public tier of GET /passport/{identifier}, GET /passport/01/{gtin}, GET /passport/01/{gtin}/10/{lot} and GET /resolve/{identifier} now carries, for a textile passport, the name and the percentage of each material (materials.<row>.name and materials.<row>.percentage), and for a footwear passport the name of the material of the upper, the lining, the insole and the sole. These are label mentions: Regulation (EU) No 1007/2011 for the fibre composition, Directive 94/11/EC for footwear. They stay public whatever the brand's access rules.

      Details

      The response you now receive. A materials block may appear in data at the public tier. The origin of the materials is not part of it.

      What you do. Nothing. If you showed "composition restricted" for want of materials, show the block when it is there.

    5. Change

      #An empty materials block is no longer rated as a declared composition

      The evidence block of GET /passport/{identifier}, GET /passport/01/{gtin}, GET /passport/01/{gtin}/10/{lot} and GET /resolve/{identifier} rated composition: "declared" as soon as the passport carried a materials block, even when every row was empty: a template saved as is was enough.

      Details

      The response you now receive. composition appears only when a material row carries at least a name, a share or an origin. Otherwise the key is absent from sections, as for a passport with no materials. A figure the brand left empty is never returned as 0.

      What you do. Nothing. Treat a missing composition as you already do: the brand declared nothing.

    6. Change

      #The product photo is served as a light copy

      image_url of the passports (GET /passport/{identifier}, GET /passport/01/{gtin}, GET /passport/01/{gtin}/10/{lot} and the matching partner bundles), of GET /products/{uid}/public, of GET /qr/verify and of GET /timeline/{identifier}, and url of the images of media in GET /resolve/{identifier}, now name a light copy of the photo instead of the original file.

      Details

      The response you receive now. An address ending in .photo.webp: a WebP image of 800 pixels at most on the long side, 100 KB at most, with no EXIF data. A photo that has no copy yet keeps the address of the original; an address of another site is returned as given. The field changes neither name nor type, and null keeps its meaning.

      What you do. Nothing. If your integration inferred the file format from the extension of the address, read the Content-Type header of the response instead.

    7. Change

      #No plan refuses a piece for a monthly volume any more

      The four plans now count pieces per calendar year. The first 6,000 of the year are included, each piece beyond them is priced, and beyond one million pieces in the year the price is set on quote. No piece is refused for a matter of volume. This applies on the Compliance + Identity offer. Only the trial account keeps a monthly volume.

      Details

      The response you receive now. On a paid plan, a batch sent to POST /v1/partner/mint/batch no longer receives the 403 whose detail.code is QUOTA_EXCEEDED and detail.resource is products. It is accepted. A trial account still receives that refusal, with "period": "monthly".

      What you do. Nothing. If your integration waited for that refusal to spread its submissions from one month to the next, you can remove that wait.

  6. #

    1. Change

      #The reseller key mints, sells and manages webhooks for its client brands

      A reseller key now acts for each of your client brands on every endpoint, with no key per brand: minting (POST /v1/partner/mint/batch, with the batch status and its pieces), sale declarations (POST /v1/partner/sellout) and webhooks (/v1/partner/webhooks), on top of the catalog and the passport display.

      Details

      The response you receive now. A batch whose rows carry the brand_id of one of your client brands is accepted. The sale and the subscription take an optional brand_id field, and the subscription list a brand_id parameter. A brand the key does not serve still receives a 403, whose detail is now "This API key cannot act for this brand." (the "Brand mismatch: ..." text is gone). A batch that mixes several brands receives a 422 whose detail.code is ONE_BRAND_PER_BATCH.

      What you do. Nothing if you did not read the refusal text. If your integration compared it to "Brand mismatch", compare the 403 code instead. With a reseller key, you may retire the keys created on your client brands: the details are on the page Fill the catalog through the API.

    2. Change

      #The passport of a model or of a lot carries the product photograph

      GET /passport/01/{gtin} and GET /passport/01/{gtin}/10/{lot} add the image_url field, which the passport of a piece already carried. The bundles GET /v1/partner/passports/01/{gtin} and GET /v1/partner/passports/01/{gtin}/10/{lot} carry it too, inside passport.

      Details

      The response you receive now. image_url is the full address of the model's photograph, computed the same way at all three levels: its main photo, or else the first image of its media library. null when the model has none. On the passport of a piece, three cases change: a photo recorded as a bare storage path now arrives as a full address, a model with no main photo shows the first image of its media library instead of null, and a model of a brand other than the passport's answers null.

      What you do. Nothing: it is one more field. Show it if your page shows the product.

  7. #

    1. Change

      #Two offers per brand: Compliance, or Compliance + Identity

      Every brand is now on the compliance offer or on the compliance_identity offer. Every existing brand is on compliance_identity, which is what it already had: nothing changes for it. The compliance offer covers the product passport by model or by lot, with no identity per piece.

      Details

      The response you receive now. For a brand on the compliance offer, every operation on a piece answers 403, with detail.code equal to OFFER_EXCLUDES_IDENTITY and detail.feature saying what was refused: creating pieces with POST /v1/partner/mint/batch, reading a chip with POST /v1/partner/nfc/verify, declaring a sale with POST /v1/partner/sellout, and the same operations in the console. Nothing is written, queued or sent to the blockchain.

      What you do. Nothing for a brand that does not change offer. For a brand on the compliance offer, do not replay a call refused with this code, and print the QR code of its model or its lot. The details are on the page Compliance, or Compliance + Identity.

    2. Change

      #The QR code of a model or a lot comes back for the Compliance offer

      GET /qr/01/{gtin}, GET /qr/01/{gtin}/download, GET /qr/01/{gtin}/10/{lot} and GET /qr/01/{gtin}/10/{lot}/download draw the code of the model or the lot again, for a brand on the compliance offer whose passport is published.

      Details

      The response you receive now. 200 and a PNG image for such a brand. In every other case, the same response as before: 410 with detail.code equal to ITEM_QR_ONLY.

      What you do. Nothing if your brands are on the compliance_identity offer: they still print the code of every piece.

    3. Change

      #A QR code to print carries the brand's domain once that domain is final

      GET /qr/product/{identifier}, GET /qr/product/{identifier}/download, GET /qr/verify/{identifier}, GET /qr/certificate/{identifier} and GET /qr/certificate/{identifier}/download, as well as the QR code on the certificate PDF and the wallet card, carry the brand's domain only when that domain is final (locked). Before that, they carry our address. A domain that is only verified can still change, and a printed code carrying it would lead nowhere after that change.

      Details

      The response you get now. The status stays 200 and the image stays a PNG. For a brand whose domain is verified but not final yet, the encoded address starts with https://sealtrust.io instead of the brand's domain. That address stays valid forever.

      What you do. Nothing. If you print these codes for a brand that wants its domain on its labels, the brand first makes its domain final from its console, by exporting the QR codes of a batch.

  8. #

    1. Change

      #The passport says which tiers are open to you

      GET /passport/{identifier}, GET /passport/01/{gtin} and GET /passport/01/{gtin}/10/{lot} add the available_tiers field: the access tiers your call may ask for on this passport, in the order public, end_user, repairer, recycler, upstream, authority. The model and lot passports also add access_tier, the tier served, until now only in the X-DPP-Access-Tier header.

      Details

      The response you now receive. The code stays 200. Without authentication, available_tiers is ["public"]. The list grants nothing: every tier asked is still checked, with the same refusals as before.

      What you do. Nothing. If you offer your users a choice of tier, offer the tiers of this list, and only them. A response made for a signed-in account must go to no shared cache.

    2. Breaking change

      #GET /resolve/{identifier} serves the public tier

      GET /resolve/{identifier} reads no session. It nevertheless served the passport at the end_user tier, to any caller. It now serves the public tier, and that tier only: the same data as GET /passport/{identifier} without an account. A call made with a session, the owner's included, gets the same answer.

      Details

      The response you now get. The status stays 200. In passport.data, the environmental impact, repairability index, expected lifetime, durability, energy efficiency, carbon footprint, primary material, care and movement no longer appear. The recyclability rate and the recycled content stay. The evidence block does not change.

      What you do. If your integration displays those fields, read them on GET /passport/{identifier} with an account session token in Authorization: Bearer. If you display them when they are missing, show an absence, never a zero: a missing value is not a zero value.

    3. Breaking change

      #The end_user access tier needs a signed-in account

      The end_user tier adds the environmental impact, circularity, primary material, durability and energy efficiency to the passport. It was served to any caller who asked for it with ?access_tier=end_user, with no session. It is now reserved to a signed-in account. The unit's current owner still gets it by default.

      Details

      The rule holds for the six endpoints that take access_tier: GET /passport/{identifier}, GET /passport/01/{gtin}, GET /passport/{identifier}/vc, GET /passport/{identifier}/vc/preview, GET /passport/{identifier}/vc/verify and GET /passport/01/{gtin}/10/{lot}.

      The response you now get. A call with no session at the end_user tier gets 401, with detail equal to End-user-tier access requires authentication. The public tier does not change.

      GET /resolve/{identifier} takes no access_tier parameter. It serves the public tier, see the previous entry.

      What you do. If your integration asks for end_user without a session, present an account session token in Authorization: Bearer, or ask for the public tier. A partner API key is not a session and does not open this tier.

    4. Breaking change

      #The partner portal answers 404 for the product of another brand

      GET /v1/partner-portal/products/{identifier} and POST /v1/partner-portal/interventions answered 403, with the message Ce produit appartient à une marque qui ne vous a pas accrédité, when the identifier was a token number, a hash or a certificate number of a brand that has not accredited you. By serial number, the same request already answered 404. The 403 confirmed that the product exists.

      Details

      The exact response. These requests now answer 404, detail is Produit introuvable, exactly as for a made-up identifier. On the own domain of a brand, the portal finds the products of that brand only. The 403 Aucune accréditation active does not change.

      What you do. If your code handles this 403 separately, handle the 404 instead: the product is outside your scope, or it does not exist.

    5. Breaking change

      #The trade tiers are reserved to the administrators of the brand

      The repairer, recycler and upstream tiers were open to any account tied to the brand of the product, even when that account was not an administrator: a former administrator turned back into a client, or turned into a repairer, still read the three tiers. They are now open to the administrators of the brand, to partners for the tier of their accreditation, and to the market surveillance authority.

      Details

      The exact response. Such an account receives 403 on GET /v1/passport/{identifier}, GET /v1/passport/01/{gtin}, GET /v1/passport/01/{gtin}/10/{lot} and on the three verifiable credential endpoints, with the usual detail of that refusal. A repairer keeps the repairer tier, and a recycler the recycler tier.

      What you do. Nothing if you call with an administrator or partner account. Otherwise, ask the brand for the role that matches your use.

    6. Change

      #Show the passport in your own page

      Six endpoints are added for the partner that shows a brand's passports in its own page. Three new permissions open them: passport:read, scans:write and nfc:verify.

      Details
      • GET /partner/passports/{identifier}, GET /partner/passports/01/{gtin} and GET /partner/passports/01/{gtin}/10/{lot} return the passport, its proof and its signature in one call.
      • POST /partner/scans and POST /partner/nfc/verify count each scan for the visitor named by four signed headers, and no longer for your server.
      • GET /partner/nfc/receipts/{receipt} reads a chip read our page has already verified: the chip is verified only once.
      • POST /partner/nfc/verify answers 503 NFC_READ_IN_PROGRESS, with Retry-After, to a retry that arrives while your first call is still verifying the same read, instead of taking it for a copy.
      • Our /claim and /transfer pages accept serial and return and send the visitor back to your page, with claim_outcome or transfer_outcome. GET /my-products now carries the serial field.

      A reseller key also acts for the client brands on these six endpoints.

      When a brand switches it on in the console, /p/{serial}, /01/{gtin}/21/{serial}, /01/{gtin} and /01/{gtin}/10/{lot} answer 302 to the partner's page, and GET /sdm/verify-url carries a partner_redirect_url field. Without that setting, these answers do not change, and the field is null.

      What you do. Nothing for an existing integration. To show the passports in your page, read Show the passport in your page.

    7. Change

      #A passport per production batch

      A passport can now be attached to a production batch, in addition to the model and the item. Three new public addresses, without a key:

      Details
      • GET /01/{gtin}/10/{lot}, the GS1 link of the batch, which answers 302 to the page of the batch's passport, or to the model's when the batch has no published passport yet;
      • GET /v1/passport/01/{gtin}/10/{lot}, the batch's passport, with "level": "batch", product_batch_id and batch_code;
      • GET /v1/passport/01/{gtin}/10/{lot}/proof, its proofs. Its anchoring on chain comes from the same sweep as the other passports, without any minting.

      A unit with no passport of its own now receives the one of its batch when it has one, before its model's, on GET /v1/passport/{identifier}, its proof, its verification, GET /v1/resolve/{identifier} and the partner portal.

      What you do. Nothing: no existing response changes as long as no batch passport is published. If your integration reads product_id: null as "model passport", read instead the level field of the shared level responses, which is model or batch.

      Three rules protect batch labels already printed:

      • the GTIN of a model whose batch has a published passport no longer changes: PUT /admin/product-models/{id} answers 409, and the Excel import refuses the row;
      • a GTIN belongs to one brand, at every level: publishing a model or batch passport under a GTIN that another brand already publishes, through its model or one of its batches, answers 409;
      • publishing a batch draft whose batch was renamed since answers 422.

      The reversibility archive writes for each passport its level, its product_batch_id and its batch_code.

    8. Change

      #Fill the catalog through the API, and one key for several brands

      Seven endpoints are added under /partner/catalog. They create or complete models, batches and the draft passport of a model, and they read the completeness score. Two new scopes open them: catalog:read and catalog:write. Every write can be replayed without a duplicate, and a field left out or set to null erases nothing. Nothing is published this way. Every write is recorded in the audit log, with the key, the brand written into and each changed field before and after. Every call, reads included, is recorded in the key's usage log. A model created this way is at unit level, as through every other path.

      Details

      A reseller key also acts for the client brands attached to the reseller contract, on these seven endpoints only.

      Every returned batch carries lot_passport_blocker: null when its code can carry a lot passport, otherwise the reason (more than 20 characters, or a character the GS1 link refuses). The batch is created all the same.

      Four scopes that opened no endpoint are no longer offered when a key is created: mint:single, products:read, products:status and transfers:create. A key that already carries one keeps working exactly as before, since that scope opened nothing.

      What you do. Nothing for an existing integration. To pre-fill your catalog, create a key carrying catalog:write and read Fill the catalog through the API.

    9. Breaking change

      #Only a unit's QR code is generated

      The code to print is each unit's own, /p/{serial}. A QR code shared by every piece of a model or of a batch is no longer generated.

      Details

      The response you get now. GET /qr/01/{gtin}, GET /qr/01/{gtin}/download, GET /qr/01/{gtin}/10/{lot} and GET /qr/01/{gtin}/10/{lot}/download answer 410, with detail.code equal to ITEM_QR_ONLY. The response is the same for every GTIN and every batch, known or not. The links /01/{gtin} and /01/{gtin}/10/{lot} themselves do not change: a code already printed still leads to its passport.

      What you do. Draw each unit's code with GET /qr/product/{serial}/download. The page of each unit shows the passport of its batch, or else the passport of its model.

    10. Change

      #One unit per piece

      A product model carries the field identity_level, which is unit: each piece gets its own token on the chain and its own QR code, for authentication, owner, resale and theft report. The passport stays carried by the model or the batch, and each unit inherits it.

      Details
      • POST /admin/product-models and PUT /partner/catalog/brands/{brand_id}/models create a model at unit level, whether the body gives the field or not. Connectors and the Excel import also create at unit level. Sending "identity_level": "model" answers 422.
      • A model still at model level gets no unit: /mint, /mint/batch, /admin/batch/mint, /admin/batch/prepare and /admin/batch/prepare-mapped answer 409 with the code MODEL_LEVEL_IDENTITY, before anything is written, whether the row names the model or only its batch. The detail gives identifiers only.
      • A row that names the model or the batch of a brand other than its brand_id answers 404 with the code UNKNOWN_PRODUCT_REFERENCE, like an identifier that does not exist.

      What you do. Do not send identity_level, or send "unit". If a 409 MODEL_LEVEL_IDENTITY answers, switch the model to unit level with PUT /admin/product-models/{id}, then send the same request again: a retry without that change gets the same answer.

  9. #

    1. Change

      #A battery's compliance score is measured against the Commission's list

      The "Compliance" dimension of the readiness score checked only three things: that the passport exists, that it is published, and that our template's required fields are filled. For a battery, it now also compares your passport against the numbered data points of the Commission guidance, and names the missing ones by their official number.

      Details

      The requirement depends on the category: 46 mandatory data points for an electric vehicle battery, 49 for a light means of transport, 32 for an industrial one. The score reads product_identity.battery_category and marks against the matching column.

      What you do. A battery model's score may drop, because it now measures what the regulation asks for and no longer only what our template offers. The cockpit lists the missing data points with their number: that is the same numbering an authority will use.

      ⚠️ A few mandatory data points have no field with us yet. They are counted neither as filled nor as missing: the cockpit shows them separately, as a gap on our side.

    2. Change

      #A product's timeline shows its addresses only to its own brand

      GET /v1/timeline/{identifier} returned email addresses and wallet addresses in the clear to any signed-in administrator account, whatever their brand. Email addresses in the clear now go only to the product's current owner and to an administrator of the brand that issued that product; the wallet addresses from_address and to_address go only to that brand administrator. Any other caller, signed in or not, receives the anonymized projection: the other parties' email addresses are masked there, and from_address and to_address are null. A SealTrust superadmin keeps the access on every product, because that is the operations account.

      Details

      What you do. Nothing, if you were calling this address for your own products. If your integration was reading the timeline of another brand's products, it will now receive the anonymized projection.

  10. #

    1. Change

      #Resolution says on what basis to believe each section of the passport

      GET /v1/resolve/{identifier} returns one more field, evidence. It says, for each rated section of the passport, whether it is checked mechanically against a public record, backed by an attached third-party document, or declared by the brand.

      Details

      It is the same block, computed by the same function, as the one GET /v1/passport/{identifier} already returns. The function is the same, the passport version read can differ: /resolve is public and reads only the versions whose visibility is public, while /passport additionally serves the owner_only versions to the current owner when that owner is authenticated. A unit that carries an owner_only version can therefore receive two different levels of proof depending on the endpoint called. For every other caller, both responses carry the same version and the same level.

      The exact response. The field is added at the end of the object, next to passport_anchor. The block below is the one of a passport that describes both its materials and its substances of concern.

      JSON
      {
        "evidence": {
          "sections": {
            "identity": "verified",
            "integrity": "verified",
            "composition": "declared",
            "substances_of_concern": "declared"
          },
          "legend": {
            "declared": "Stated by the brand. Recorded, dated and attributable, but not independently checked.",
            "verified": "Rests on an on-chain reference that anyone can re-check independently. SealTrust records it and does not re-query the chain on each request."
          },
          "derived": true,
          "note": "Statuses are computed by SealTrust from verifiable facts. A brand cannot set them."
        }
      }

      It reads null when no published passport is served, and null if its computation fails.

      Four sections can be rated, and no other part of the passport receives a status. identity and integrity are always there, because they bear on the unit itself. composition and substances_of_concern appear only when the passport data carries them, so sections holds two to four entries depending on the unit. Read the keys that are there, assume none.

      What you do. Nothing. The field is added, no field changes meaning and no call that worked before breaks. A client that does not read evidence receives the same response as before. If you decide to display these statuses, display one only for the sections that sections names, and do not copy these values into your database: they are recomputed on each read.

  11. #

    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.

  12. #

    Two API fixes. Neither is a breaking change, but both cancel advice this documentation used to give.

    1. Change

      #Batch tracking finally says a batch failed

      A batch where no line went through replied status: "finished", because the execution queue was the one speaking. The same address then switched to failed once the queue retention ran out, about eight minutes later. Same batch, same run, two opposite answers depending on when you looked.

      Details

      Now, as soon as your reservation carries a terminal state, it decides: a batch where no line went through replies status: "failed". And the four counters batch_status, items_count, success_count and error_count are present in every reply, from the first call on, where they used to appear only once the queue had forgotten the batch.

      What you do. Base your decision on success_count and error_count. If you followed our old advice not to look at status: "failed", drop that exception: the status is reliable now.

    2. Change

      #Rate headers are no longer emitted twice

      Three x-ratelimit-* headers could come out doubled when two layers wrote them. Only one writes them now, and the safety net stands down when another layer has already written the family.

      Details

      What you do. If you were splitting these values on the comma, as this documentation advised, you no longer need to. Reading them straight as an integer works.

  13. #

    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.

  14. #

    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.

  15. #

    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 'batch-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.

  16. #

    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.

  17. #

    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.

  18. #

    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.

  19. #

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

  20. #

    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.

  21. #

    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.

  22. #

    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