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.
{
"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
- How this page is built
- Why the version number does not change
- October 8, 2026
- A piece keeps its identity when its brand changes offer
- The admin MCP server is added to Claude with your account
- October 6, 2026
- Breaking change: every notification is signed, the secret is mandatory
- A retry follows the subscription as it is when it leaves
- The MCP server is added to Claude and ChatGPT with an address
- The catalog by key reads batches back, lists passports, previews and gives templates
- October 5, 2026
- The owner's product list shows the model's photo
- Claiming by serial number a piece you already own
- The signed credential of a model or a lot has its address
- September 30, 2026
- The size, colour and weight of a passport are public
- The signed credential discloses what the page shows
- Breaking change: a brand number is written with digits only
- Purchase codes are issued in bulk through the API
- Verifying a signed QR code no longer says "authentic"
- The public read of a product gives its registration date and its certificate state
- The PDF certificate no longer says "authentic, blockchain verified"
- September 29, 2026
- A brand is named by its public code in the partner API
- The address to paste into your ERP names the brand by its public code
- The owner's space gives the brand's public code
- A textile's composition and a shoe's materials are public
- An empty materials block is no longer rated as a declared composition
- The product photo is served as a light copy
- No plan refuses a piece for a monthly volume any more
- September 28, 2026
- The reseller key mints, sells and manages webhooks for its client brands
- The passport of a model or of a lot carries the product photograph
- September 27, 2026
- Two offers per brand: Compliance, or Compliance + Identity
- The QR code of a model or a lot comes back for the Compliance offer
- A QR code to print carries the brand's domain once that domain is final
- September 26, 2026
- The passport says which tiers are open to you
- Breaking change. GET /resolve/{identifier} serves the public tier
- Breaking change. The end_user access tier needs a signed-in account
- Breaking change. The partner portal answers 404 for the product of another brand
- Breaking change. The trade tiers are reserved to the administrators of the brand
- Show the passport in your own page
- A passport per production batch
- Fill the catalog through the API, and one key for several brands
- Breaking change. Only a unit's QR code is generated
- One unit per piece
- August 30, 2026
- A battery's compliance score is measured against the Commission's list
- A product's timeline shows its addresses only to its own brand
- August 29, 2026
- Resolution says on what basis to believe each section of the passport
- August 26, 2026
- Breaking change. The value_chain access tier no longer exists
- The partner portal announces the trade it served
- A unit's public image falls back to its model
- August 23, 2026
- Batch tracking finally says a batch failed
- Rate headers are no longer emitted twice
- August 22, 2026
- The public certificate names the issuing brand
- Authentication refusals answer in English
- The partner portal has a rate limit
- August 21, 2026
- Sending a batch now creates products
- August 20, 2026
- Breaking change. The API rejects the owner_email and contract_address fields
- Breaking change. Replaying an idempotency key with a different batch answers 409
- Breaking change. Batch tracking requires the mint:batch scope
- Breaking change. Changing a notification subscription requires your plan to include them
- An unreadable JSON body answers 400
- The eight key-authenticated endpoints are rate limited
- Your notification subscriptions stay readable and deletable whatever your plan
- August 16, 2026
- Prefix-free addresses become permanent aliases
- August 14, 2026
- A damaged gzip file answers 400
- Batch tracking returns four more fields when the queue has forgotten your batch
- August 13, 2026
- A partner portal intervention carries a proof level
- August 11, 2026
- Breaking change. Deleting a subscription requires retyping its address
- Every delivery carries the event type
- July 14, 2026
- Two headers are added to every delivery
- The signature covers the exact bytes sent to you
- July 12, 2026
- Subscription endpoints require scopes
- March 15, 2026
- The /v1 prefix appears
- What this page does not do
- 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
403OFFER_EXCLUDES_IDENTITYfor 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}andGET /my-productscarryidentity_includedset totruefor these pieces. Creating a piece, ordering chip encoding and issuing a new purchase code still answer403OFFER_EXCLUDES_IDENTITYon 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_includedinstead. - 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/mcpanswers 401 with aWWW-Authenticateheader 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 aRetry-Afterheader. 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.
- 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 suppliedsecretis 32 to 128 characters long. Without asecret, we generate one of 43 characters. OnPUT /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
nullsecret in a modification, withlocnamingsecret. 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
secretfield of the response and configure it on your receiver. To keep the current secret in a modification, omit the field instead of sendingnull. - 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.
- 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 aGETor aDELETEanswers 405. A request carrying anOriginheader 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 aRetry-Afterheader. 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.
- 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:readscope: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}/templatesandGET /partner/catalog/brands/{brand_code}/templates/{template_key}, the passport templates.
The draft passport write accepts one more field,
only_if_draft. Set totrue, 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.
- Change
#The owner's product list shows the model's photo
GET /my-productsansweredmedia: []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
medialist holds no image, the model's cover image is added to it first, with the identifier0,media_typeset toimageand 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
mediaas the thumbnail, it now appears. Do not look for the entry with identifier0in the brand's media library: it has no record of its own. - Change
#Claiming by serial number a piece you already own
POST /claims/from-serialanswered409"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
409withdetail"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. - 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 atGET /passport/01/{gtin}/10/{lot}/vc. Each one also has its/vc/verifyform and its/vc/previewform, 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}/vcand its two neighbours, withlevel(modelorbatch),gtinand, for a lot,batch_codein addition.What you do. Nothing. To verify the passport of a model or a lot, call the new address.
- Change
#The size, colour and weight of a passport are public
The
size,colorandweightfields written at the root of a passport are now returned at thepublictier, onGET /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.
- Change
#The signed credential discloses what the page shows
GET /passport/{identifier}/vcand its per-tier presentation now follow exactly the rules ofGET /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
publictier, a textile's credential carriescompositionwith thematerialandpercentageof 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.
- 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"andtrue, 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.codeisBRAND_REF_NOT_WELL_FORMED.detail.referencessays 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 withBRAND_REF_NOT_WELL_FORMED. The whole batch is refused and nothing is created.What you do. Send the number with digits only,
5or"5", or better, thebrand_codethatGET /v1/partner/catalog/brandsgives. If your code produces a decimal number, convert it to an integer before sending. - Change
#Purchase codes are issued in bulk through the API
A key carrying the new
claim_codes:issuescope 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, withPOST /partner/claim-codes/brands/{brand_code}/status.POST /partner/mint/batch?issue_purchase_codes=trueasks for a code per piece at mint time, andGET /partner/mint/batch/{job_id}/itemsgives them once, inpurchase_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 404PIECE_NOT_FOUNDas an unknown piece. The response ofPOST /partner/mint/batchcarries a sixth field,issue_purchase_codes.What you do. Nothing if you do not use purchase codes. Otherwise, tick
claim_codes:issueon a dedicated key, never ticked by default, and read Issue purchase codes. - Change
#Verifying a signed QR code no longer says "authentic"
GET /qr/verifyno 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
validfield.messageis "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 displayedmessage, never display "authentic" from this response. - Change
#The public read of a product gives its registration date and its certificate state
GET /products/{uid}/publicreturns two more fields.registered_atcarries the date the product's record was created, under a name that says what it is.certificate_statuscarries the state of the certificate of authenticity in force, read exactly asGET /resolve/{identifier}reads it:active, ornullwith no certificate in force.verified_atstays, with the same value asregistered_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_atas a verification date, readregistered_atand write "registered on": nobody verified the object on that date. - Change
#The PDF certificate no longer says "authentic, blockchain verified"
The watermark seal of
GET /certificate/{identifier}/downloadnow readsREGISTEREDandON A PUBLIC BLOCKCHAIN(in FrenchENREGISTRÉandSUR UNE BLOCKCHAIN PUBLIQUE). It readAUTHENTICandBLOCKCHAIN 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.
- 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 example7K3QXW9M2A. The partner API now names the brand by this code: in the path of the/partner/catalog/brands/{brand_code}/...routes, in the body ofPOST /partner/selloutand ofPOST /partner/webhooks, in the listGET /partner/webhooks?brand_code=, and on every row ofPOST /partner/mint/batch.GET /partner/catalog/brandsgives the code of every brand your key may act for. The public routesGET /brands/{brand_code}/brandingandGET /brand/{brand_code}/pubkeyalso accept the code.Details
The response you now receive. The answers of the partner API, of the partner portal, of
/sdm/verifyand of the public keys that returnedbrand_idalso returnbrand_code.brand_idis deprecated there. A code that names no brand gets the same 403 as a brand the key does not serve. A request that sendsbrand_codeandbrand_idtogether is refused: with 422 for a sale or a subscription, with 400 for a batch row.What you do. Nothing right away: the
brand_idnumber is still accepted everywhere it was. Replace it withbrand_codeat your next change.GET /brand/{brand_id}/did.jsonkeeps the number, because it is part of thedid:webidentifier already signed into the issued credentials. - 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.
- Change
#The owner's space gives the brand's public code
The answers of the owner's space that returned
brand_idalso returnbrand_code:GET /my-products, its history, the brand listGET /my-products/products/brands, the/ownership/...routes, the buybacks, the returns, the messages and the handover codes/custody/repair-codes. TheGET /my-products/export/csvexport gains abrand_codecolumn, placed right afterbrand_id.Details
The response you now receive.
brand_idis deprecated there.GET /my-productsaccepts the?brand_code=filter.What you do. Nothing right away:
brand_idis still returned. Readbrand_codeat your next change. If you read the export by column position, shift every column that followsbrand_idby one. - Change
#A textile's composition and a shoe's materials are public
dataat thepublictier ofGET /passport/{identifier},GET /passport/01/{gtin},GET /passport/01/{gtin}/10/{lot}andGET /resolve/{identifier}now carries, for a textile passport, the name and the percentage of each material (materials.<row>.nameandmaterials.<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
materialsblock may appear indataat thepublictier. 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. - Change
#An empty materials block is no longer rated as a declared composition
The
evidenceblock ofGET /passport/{identifier},GET /passport/01/{gtin},GET /passport/01/{gtin}/10/{lot}andGET /resolve/{identifier}ratedcomposition: "declared"as soon as the passport carried amaterialsblock, even when every row was empty: a template saved as is was enough.Details
The response you now receive.
compositionappears only when a material row carries at least a name, a share or an origin. Otherwise the key is absent fromsections, as for a passport with no materials. A figure the brand left empty is never returned as0.What you do. Nothing. Treat a missing
compositionas you already do: the brand declared nothing. - Change
#The product photo is served as a light copy
image_urlof the passports (GET /passport/{identifier},GET /passport/01/{gtin},GET /passport/01/{gtin}/10/{lot}and the matching partner bundles), ofGET /products/{uid}/public, ofGET /qr/verifyand ofGET /timeline/{identifier}, andurlof the images ofmediainGET /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, andnullkeeps its meaning.What you do. Nothing. If your integration inferred the file format from the extension of the address, read the
Content-Typeheader of the response instead. - 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/batchno longer receives the403whosedetail.codeisQUOTA_EXCEEDEDanddetail.resourceisproducts. 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.
- 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_idof one of your client brands is accepted. The sale and the subscription take an optionalbrand_idfield, and the subscription list abrand_idparameter. A brand the key does not serve still receives a403, whosedetailis now"This API key cannot act for this brand."(the"Brand mismatch: ..."text is gone). A batch that mixes several brands receives a422whosedetail.codeisONE_BRAND_PER_BATCH.What you do. Nothing if you did not read the refusal text. If your integration compared it to
"Brand mismatch", compare the403code 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. - Change
#The passport of a model or of a lot carries the product photograph
GET /passport/01/{gtin}andGET /passport/01/{gtin}/10/{lot}add theimage_urlfield, which the passport of a piece already carried. The bundlesGET /v1/partner/passports/01/{gtin}andGET /v1/partner/passports/01/{gtin}/10/{lot}carry it too, insidepassport.Details
The response you receive now.
image_urlis 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.nullwhen 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 ofnull, and a model of a brand other than the passport's answersnull.What you do. Nothing: it is one more field. Show it if your page shows the product.
- Change
#Two offers per brand: Compliance, or Compliance + Identity
Every brand is now on the
complianceoffer or on thecompliance_identityoffer. Every existing brand is oncompliance_identity, which is what it already had: nothing changes for it. Thecomplianceoffer 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
complianceoffer, every operation on a piece answers403, withdetail.codeequal toOFFER_EXCLUDES_IDENTITYanddetail.featuresaying what was refused: creating pieces withPOST /v1/partner/mint/batch, reading a chip withPOST /v1/partner/nfc/verify, declaring a sale withPOST /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
complianceoffer, 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. - 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}andGET /qr/01/{gtin}/10/{lot}/downloaddraw the code of the model or the lot again, for a brand on thecomplianceoffer whose passport is published.Details
The response you receive now.
200and a PNG image for such a brand. In every other case, the same response as before:410withdetail.codeequal toITEM_QR_ONLY.What you do. Nothing if your brands are on the
compliance_identityoffer: they still print the code of every piece. - 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}andGET /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
200and the image stays a PNG. For a brand whose domain is verified but not final yet, the encoded address starts withhttps://sealtrust.ioinstead 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.
- Change
#The passport says which tiers are open to you
GET /passport/{identifier},GET /passport/01/{gtin}andGET /passport/01/{gtin}/10/{lot}add theavailable_tiersfield: the access tiers your call may ask for on this passport, in the orderpublic,end_user,repairer,recycler,upstream,authority. The model and lot passports also addaccess_tier, the tier served, until now only in theX-DPP-Access-Tierheader.Details
The response you now receive. The code stays
200. Without authentication,available_tiersis["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.
- Breaking change
#
GET /resolve/{identifier}serves thepublictierGET /resolve/{identifier}reads no session. It nevertheless served the passport at theend_usertier, to any caller. It now serves thepublictier, and that tier only: the same data asGET /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. Inpassport.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. Theevidenceblock does not change.What you do. If your integration displays those fields, read them on
GET /passport/{identifier}with an account session token inAuthorization: Bearer. If you display them when they are missing, show an absence, never a zero: a missing value is not a zero value. - Breaking change
#The
end_useraccess tier needs a signed-in accountThe
end_usertier 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/verifyandGET /passport/01/{gtin}/10/{lot}.The response you now get. A call with no session at the
end_usertier gets401, withdetailequal toEnd-user-tier access requires authentication. Thepublictier does not change.GET /resolve/{identifier}takes noaccess_tierparameter. It serves thepublictier, see the previous entry.What you do. If your integration asks for
end_userwithout a session, present an account session token inAuthorization: Bearer, or ask for thepublictier. A partner API key is not a session and does not open this tier. - Breaking change
#The partner portal answers 404 for the product of another brand
GET /v1/partner-portal/products/{identifier}andPOST /v1/partner-portal/interventionsanswered 403, with the messageCe 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,
detailisProduit 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 403Aucune accréditation activedoes 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.
- Breaking change
#The trade tiers are reserved to the administrators of the brand
The
repairer,recyclerandupstreamtiers 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 usualdetailof that refusal. A repairer keeps therepairertier, and a recycler therecyclertier.What you do. Nothing if you call with an administrator or partner account. Otherwise, ask the brand for the role that matches your use.
- 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:writeandnfc:verify.Details
GET /partner/passports/{identifier},GET /partner/passports/01/{gtin}andGET /partner/passports/01/{gtin}/10/{lot}return the passport, its proof and its signature in one call.POST /partner/scansandPOST /partner/nfc/verifycount 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/verifyanswers503NFC_READ_IN_PROGRESS, withRetry-After, to a retry that arrives while your first call is still verifying the same read, instead of taking it for a copy.- Our
/claimand/transferpages acceptserialandreturnand send the visitor back to your page, withclaim_outcomeortransfer_outcome.GET /my-productsnow carries theserialfield.
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}answer302to the partner's page, andGET /sdm/verify-urlcarries apartner_redirect_urlfield. Without that setting, these answers do not change, and the field isnull.What you do. Nothing for an existing integration. To show the passports in your page, read Show the passport in your page.
- 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 answers302to 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_idandbatch_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: nullas "model passport", read instead thelevelfield of the shared level responses, which ismodelorbatch.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}answers409, 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, itsproduct_batch_idand itsbatch_code. - 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:readandcatalog:write. Every write can be replayed without a duplicate, and a field left out or set tonullerases 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 atunitlevel, 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:nullwhen 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:statusandtransfers: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:writeand read Fill the catalog through the API. - 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}andGET /qr/01/{gtin}/10/{lot}/downloadanswer410, withdetail.codeequal toITEM_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. - Change
#One unit per piece
A product model carries the field
identity_level, which isunit: 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-modelsandPUT /partner/catalog/brands/{brand_id}/modelscreate a model atunitlevel, whether the body gives the field or not. Connectors and the Excel import also create atunitlevel. Sending"identity_level": "model"answers422.- A model still at
modellevel gets no unit:/mint,/mint/batch,/admin/batch/mint,/admin/batch/prepareand/admin/batch/prepare-mappedanswer409with the codeMODEL_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_idanswers404with the codeUNKNOWN_PRODUCT_REFERENCE, like an identifier that does not exist.
What you do. Do not send
identity_level, or send"unit". If a409MODEL_LEVEL_IDENTITYanswers, switch the model tounitlevel withPUT /admin/product-models/{id}, then send the same request again: a retry without that change gets the same answer.
- 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_categoryand 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.
- 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 addressesfrom_addressandto_addressgo only to that brand administrator. Any other caller, signed in or not, receives the anonymized projection: the other parties' email addresses are masked there, andfrom_addressandto_addressarenull. 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.
- 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:/resolveis public and reads only the versions whose visibility ispublic, while/passportadditionally serves theowner_onlyversions to the current owner when that owner is authenticated. A unit that carries anowner_onlyversion 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
nullwhen no published passport is served, andnullif its computation fails.Four sections can be rated, and no other part of the passport receives a status.
identityandintegrityare always there, because they bear on the unit itself.compositionandsubstances_of_concernappear only when the passport data carries them, sosectionsholds 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
evidencereceives the same response as before. If you decide to display these statuses, display one only for the sections thatsectionsnames, and do not copy these values into your database: they are recomputed on each read.
Three changes. The first is a breaking change, and it concerns every accredited partner.
- Breaking change
#The
value_chainaccess tier no longer existsThe
value_chaintier 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 therecyclertier denies. That door is closed.Details
Six tiers remain:
public,end_user,repairer,recycler,upstreamandauthority. The three trade tiers are distinct audiences: holding one accreditation opens none of the others.The exact response.
?access_tier=value_chainnow answers 422, onGET /v1/passport/{identifier}, onGET /v1/passport/01/{gtin}and on the three verifiable credential endpoints.detailis the usual validation list, each entry carryingloc,typeandmsg, andlocnamesaccess_tier.What you do. Ask for your own trade's tier,
repairerorrecycler. You will receive fewer fields than before, and they are precisely the fields that are not part of your trade. - Change
#The partner portal announces the trade it served
In
GET /v1/partner-portal/products/{identifier}, thepassport.access_tierfield always readvalue_chain. It now carries the trades actually served, joined by a+and sorted alphabetically:recycler, orrepairer+recyclerfor an account that holds both accreditations on that brand. It readspublicwhen 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. - Change
#A unit's public image falls back to its model
In
GET /v1/products/{uid_hash}/public, theimage_urlfield only looked at media attached directly to the unit, and readnullas 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
nullmay now carry an address.
Two API fixes. Neither is a breaking change, but both cancel advice this documentation used to give.
- 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 tofailedonce 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 countersbatch_status,items_count,success_countanderror_countare 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_countanderror_count. If you followed our old advice not to look atstatus: "failed", drop that exception: the status is reliable now. - 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.
Three changes shipped together. None is a breaking change.
- Change
#The public certificate names the issuing brand
The
issuer_namefield carries the name of the brand that issued the certificate. The server computes it on read, and it readsnullwhen the certificate is attached to no brand.Details
Three surfaces are affected:
GET /v1/certificate/{identifier}, the PDF returned byGET /v1/certificate/{identifier}/download, and thecertificateblock ofGET /v1/resolve/{identifier}.What you do. Nothing in your calls. If you copied the value of
issuer_nameinto your own database, replace it with what the API returns today. - 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
detailare now in English:Not authenticated,Invalid token,Invalid token: missing email,Token has been revoked,Invalid JWT token,Account disabled,User not foundandPartner account required (repairer or recycler).Details
Two refusals from
POST /v1/originality/read-sig/verifyfollow the same rule and now readInvalid uid_hexandInvalid 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.
- Change
#The partner portal has a rate limit
The four
/partner-portalendpoints 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-RemainingandX-RateLimit-Reset. Going over returns 429, withdetailreadingRate limit exceeded: 30 requests per 60sand aRetry-Afterheader reading60.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.
- Change
#Sending a batch now creates products
POST /v1/partner/mint/batchanswered200, 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
qrand 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.
Seven changes shipped together. The first four are breaking changes.
- Breaking change
#The API rejects the
owner_emailandcontract_addressfieldsA 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_idandmetadata_uri, which are required, andexternal_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 byGET /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.
- Breaking change
#Replaying an idempotency key with a different batch answers 409
The
Idempotency-Keyheader 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 receives400.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
409code also signals that a call carrying this key is still being processed. The message is thenA 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-Keyvalue per distinct batch. If your code reuses a fixed value, or a value derived from the time to the minute, change it now. - Breaking change
#Batch tracking requires the
mint:batchscopeGET /v1/partner/mint/batch/status/{job_id}requires themint:batchscope, the same one as sending the batch. A key that does not carry it receives403, 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
403with theQUOTA_EXCEEDEDcode. - 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 with403.Details
JSON { "detail": { "code": "FEATURE_NOT_AVAILABLE", "feature": "webhooks" } }Turning one off is the one exception: sending
is_activeatfalseand 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_activeatfalsealone, or delete them. - Change
#An unreadable JSON body answers 400
A malformed body sent to
POST /v1/partner/mint/batchnow answers400, 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
5xxfamily, this case no longer triggers it. A request rejected with400must be fixed, and replaying it as is will give the same refusal. - 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
429with theRetry-Afterheader, expressed in seconds. When the rate limiting service is unavailable, you receive503, 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-ResetandX-RateLimit-Scope.What you do. Read
X-RateLimit-Remainingand 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 the503as a temporary outage and retry after a delay. - 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.
- Change
#Prefix-free addresses become permanent aliases
The copies without the
/v1prefix used to answer with two headers announcing an end of service on September 1, 2026,DeprecationandSunset. The API no longer sends these two headers. The/v1addresses 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
/v1form for any new integration: it is the form documented everywhere on this site.
- Change
#A damaged gzip file answers 400
POST /v1/partner/mint/batchaccepts 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.
- Change
#Batch tracking returns four more fields when the queue has forgotten your batch
GET /v1/partner/mint/batch/status/{job_id}used to answerunknownto 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_countanderror_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.
- Change
#A partner portal intervention carries a proof level
POST /v1/partner-portal/interventionsnow records a proof level with every intervention, in theproof_levelfield.Details
The request body accepts an optional
proof_codefield. Without it, the level readsdeclared. With a valid code, it readscustomer_codewhen the code was read by the customer, orwork_orderwhen it comes from the brand's work order. A rejected code answers422and records nothing at all: neither the intervention, nor the consumption of the code.Interventions recorded before this date return
proof_levelempty. An empty value does not meandeclared: 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.
- Breaking change
#Deleting a subscription requires retyping its address
DELETE /v1/partner/webhooks/{webhook_id}requires aconfirmquery parameter containing the subscription's exact address.Details
Without that parameter, a subscription that belongs to you answers
400with theCONFIRMATION_REQUIREDcode. With a value that does not match, the response is400with theCONFIRMATION_MISMATCHcode, and the API changes nothing. A successful deletion answers204.An identifier that designates no subscription of your brand answers
404, whether you sentconfirmor 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 inconfirm.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" - 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.
- Change
#Two headers are added to every delivery
Every delivery now carries
X-Webhook-IdandX-Webhook-Timestamp, in addition toX-Webhook-Signature.Details
X-Webhook-Idis 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-Eventsince August 11, 2026, andX-Webhook-Id. Never discard a delivery on the value ofX-Webhook-Timestamp: that timestamp changes with every retry, so it would make you process the same delivery twice. - Change
#The signature covers the exact bytes sent to you
X-Webhook-Signaturehas the formt=<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.
- Change
#Subscription endpoints require scopes
Listing your subscriptions and reading one require the
webhooks:readscope. Creating, changing and deleting a subscription require thewebhooks:writescope.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:batchscope alone, create a new key that also carries the notification scopes.
- Change
#The
/v1prefix appearsEvery business endpoint becomes reachable at two addresses, with the
/v1prefix and with no prefix at all.Details
The prefix-free addresses additionally answer two headers,
Deprecation: trueandSunset: 2026-09-01. The/v1addresses 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.
Was this page helpful?
Your answer opens a pre-filled email in your mail app, addressed to contact@sealtrust.io. You read it over before sending it.