Show the passport in your page
Show a brand's passport in your own page and lose nothing: the passport, its proof and its signature in one call, your visitors' scans and NFC reads counted for them, the printed QR that leads to your page, and the claim and the transfer in a hosted window that comes back to you. Permissions passport:read, scans:write and nfc:verify.
On this page
This page is for the partner that shows a brand's passports in its own page, with its own design: a reseller for its client brands, an agency for the brand it serves. When you leave it, you will know how to read a complete passport in one call, have each scan counted for the visitor who made it, verify an NFC chip only once, receive printed QR scans on your page, and have a product claimed without ever asking your visitor for their SealTrust password.
The page hosted by SealTrust stays the default option. Nothing below changes for a brand that has not asked for it.
#What you need
- An API key that carries the permissions you need:
passport:readto read,scans:writeto record scans,nfc:verifyfor NFC chips. You create it in the console, Settings then Developers. - A plan that includes API access.
- For a reseller, a reseller key: it acts for your brand and for each of the client brands of your contract.
- Your server. All these calls leave from your server, never from your visitor's browser: your key must never reach a page.
#The six endpoints
Every address also exists with the /v1 prefix, which is the recommended
form.
| Method and path | What it does | Permission |
|---|---|---|
GET /partner/passports/{identifier} | the passport of a unit, its proof and its signature | passport:read |
GET /partner/passports/01/{gtin} | the passport of a model and its proof | passport:read |
GET /partner/passports/01/{gtin}/10/{lot} | the passport of a batch and its proof | passport:read |
POST /partner/scans | records a scan made by one of your visitors | scans:write |
POST /partner/nfc/verify | verifies a chip read for one of your visitors | nfc:verify |
GET /partner/nfc/receipts/{receipt} | reads the result of a read our page verified | nfc:verify |
Each endpoint answers only for the brands your key may serve. A product of another brand gets the same 404 as an identifier that does not exist.
#The passport, its proof and its signature in one call
{identifier} is the serial number printed on the product, its chip
fingerprint or its token number.
curl https://api.sealtrust.io/v1/partner/passports/K4QNAFCHDETK \
-H "Authorization: Bearer st_live_0000000000000000000000000000000000000000000000"{
"level": "unit",
"serial": "K4QNAFCHDETK",
"passport": { "data": {}, "data_hash": "…", "gs1_digital_link": "…" },
"proof": { "data_hash": "…", "anchor": {}, "vc": {} },
"credential": { "format": "dc+sd-jwt", "issuer": "did:web:…", "sd_jwt_vc": "…" }
}passportis exactly the public answer ofGET /passport/{identifier}at thepublicaccess tier.proofis exactly the answer ofGET /passport/{identifier}/proof.credentialis exactly the answer ofGET /passport/{identifier}/vc, ornullas long as the brand has not issued a signed credential for this passport.
Your page therefore shows what the hosted page shows an anonymous visitor, no
more and no less. For a model or a batch, credential is always null, and
level is model or lot.
#Counting each scan for the visitor who made it
Your calls leave from your server, so from your address. With nothing else to go on, all your visitors would have your address: two people scanning the same product within the same half hour would count as a single scan, and every scan would be located at your hosting provider.
POST /partner/scans and POST /partner/nfc/verify therefore ask for four
headers that name the visitor.
| Header | What it contains |
|---|---|
X-SealTrust-Visitor-IP | your visitor's address, as your server saw it |
X-SealTrust-Visitor-UA | their browser's user agent, optional |
X-SealTrust-Visitor-Timestamp | the signing time, in seconds since 1970 |
X-SealTrust-Visitor-Signature | v1= followed by the signature, in hexadecimal |
The signature is an HMAC-SHA256. Its key is the SHA-256 fingerprint of your API
key, written in lowercase hexadecimal. The signed message is made of six lines
separated by a line feed: v1, the time, the method in capitals, the path
called without its part after ?, the visitor's address, and their user agent
(an empty line when there is none).
import hashlib, hmac, time, requests
API_KEY = "st_live_0000000000000000000000000000000000000000000000"
path = "/v1/partner/scans"
ip, ua = "203.0.113.77", "Mozilla/5.0"
ts = str(int(time.time()))
key = hashlib.sha256(API_KEY.encode()).hexdigest().encode()
message = "\n".join(["v1", ts, "POST", path, ip, ua]).encode()
signature = hmac.new(key, message, hashlib.sha256).hexdigest()
requests.post(
"https://api.sealtrust.io" + path,
json={"serial": "K4QNAFCHDETK"},
headers={
"Authorization": f"Bearer {API_KEY}",
"X-SealTrust-Visitor-IP": ip,
"X-SealTrust-Visitor-UA": ua,
"X-SealTrust-Visitor-Timestamp": ts,
"X-SealTrust-Visitor-Signature": f"v1={signature}",
},
timeout=10,
)import { createHash, createHmac } from "node:crypto";
const apiKey = "st_live_0000000000000000000000000000000000000000000000";
const path = "/v1/partner/scans";
const ip = "203.0.113.77";
const ua = "Mozilla/5.0";
const ts = String(Math.floor(Date.now() / 1000));
const key = createHash("sha256").update(apiKey).digest("hex");
const signature = createHmac("sha256", key)
.update(["v1", ts, "POST", path, ip, ua].join("\n"))
.digest("hex");
await fetch("https://api.sealtrust.io" + path, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
"X-SealTrust-Visitor-IP": ip,
"X-SealTrust-Visitor-UA": ua,
"X-SealTrust-Visitor-Timestamp": ts,
"X-SealTrust-Visitor-Signature": `v1=${signature}`,
},
body: JSON.stringify({ serial: "K4QNAFCHDETK" }),
});A signature is good for one method, one path and five minutes. Sign each call at the moment you send it.
The body of POST /partner/scans names the product by serial or by
uid_hash, never both. It also accepts latitude and longitude when the
visitor's device provided them. From the address we only keep a city and a
country, and we store an anonymised address.
The same visitor coming back to the same product within the half hour counts
as a single scan. The answer is then {"ok": true, "deduped": true}.
#A single NFC verification per read
An NFC chip produces, at each read, a single-use address. Verifying it twice makes the second verification answer as a copy of the address, which shows as a possible clone of a genuine product. Two cases arise.
The read opens our page. That is the case of the chips already in
circulation: their address leads to SealTrust. When the brand shows its
passports at your place, our page verifies the read once, then sends the
visitor to your page with level=nfc, serial and receipt. Your server
reads the result with that receipt:
curl https://api.sealtrust.io/v1/partner/nfc/receipts/Zr3Kx0vQ8a1bW9c2dE7fG4hJ6kL5mN0p \
-H "Authorization: Bearer st_live_0000000000000000000000000000000000000000000000"Never send that read to POST /partner/nfc/verify: it has already been
verified. A receipt can be read for ten minutes, as many times as needed.
Your own reader read the chip. Send e, c and, for a sealed chip, t,
as the chip wrote them, with the four visitor headers, to
POST /partner/nfc/verify. If your call fails on the way, send the same read
again with the same key: for ten minutes, the answer gives back the first
result with repeat: true, instead of taking it for a copy. If your first
call is still running when the retry arrives, the retry waits a few seconds
for its result, or answers 503 NFC_READ_IN_PROGRESS with a Retry-After
header: send it again after that delay, and it gives back the first result.
Both answers carry the same fields: verified, serial, uid_hash, ctr,
brand_code, brand_id, product_name, token_id, declared_stolen,
tamper_status, seal_intact, tamper_policy, scan_area and verified_at.
brand_code is the brand's public code. brand_id, its internal number, is
deprecated: read brand_code. For the passport
itself, call GET /partner/passports/{serial} next.
#The printed QR leads to your page
In the console, Settings then Developers, the "Passport shown in a partner's page" block declares your web addresses and the page that shows a passport. A box then sends printed QR scans to that page. A reseller sets it once on its own brand, and the setting holds for all the client brands of its contract, except those that have their own.
The printed links /p/{serial}, /01/{gtin}/21/{serial}, /01/{gtin} and
/01/{gtin}/10/{lot} then answer 302 to your page, with these parameters:
| Parameter | Value |
|---|---|
level | unit, model or lot |
serial | the serial number, for a unit |
gtin and lot | for a model or a batch |
lang | fr or en, the reader's language |
Two requests keep our passport: a machine asking for the passport itself
(?linkType=dpp), and a withdrawn unit. The scan of a unit is recorded at the
moment of the redirect, for the visitor. If your page also records it with
POST /partner/scans for the same visitor, it counts only once. Robots that
follow a link to show its preview (WhatsApp, Slack, LinkedIn, search engines)
are redirected, but do not count as a scan.
A client brand whose plan does not include API access still sees, in this block, that its scans go to its reseller's page. It can keep them on the hosted page: the "Keep QR scans on the hosted page" button does not need API access.
#The claim in a hosted window
Claiming a product needs a SealTrust account. Never ask for your visitor's password: open our claim page, in a new window or by sending the visitor there.
https://sealtrust.io/en/claim?serial=K4QNAFCHDETK&return=https%3A%2F%2Fyour-site.example%2FpassportA visitor who has no account yet creates one from this page. The serial number
and return follow them to the confirmation email, then to sign-in: they come
back to the claim screen of this product without typing anything again. The
"Sign in" and "Sign up" links at the top of the page carry them too as soon as
it shows, before it has finished loading: a visitor on a slow mobile network
who clicks at once loses nothing.
return is the page to come back to. It must sit on one of the web addresses
declared in the console for this brand, or on the verified domain that serves
its public pages, otherwise no back button appears. When
you have just read the chip, add the read after a #, for instance
#e=…&c=…, so the visitor does not have to read it again: the read does not
leave their browser.
Our page never claims on its own. It takes the read passed after the # only
when return is accepted for this product, then shows a "Claim this product"
button: the claim starts on the visitor's click, not before. Without an
accepted return, the read is ignored.
This read serves for five minutes after it was verified. After that, or if the visitor takes longer to sign in, they read the chip again on our page. It is the same rule as on the hosted page: a chip read is not replayed beyond a few minutes.
Once the claim is done, the "Back to your-site.example" button brings the visitor back:
- if you opened the window with
window.open, your page receives a message{ "type": "passport-claim", "outcome": "claimed", "serial": "K4QNAFCHDETK" }addressed to the exact origin of thereturnpage, and the window closes. So open the window from a page of that same origin; - otherwise, the visitor comes back to
return, withclaim_outcome=claimedadded to the address.
outcome takes the values claimed, submitted, already_owned, expired,
not_activated, window_closed, code_not_issued, unavailable or failed.
#The transfer in a hosted window
The owner transfers their product the same way, on our transfer page, with the same parameters:
https://sealtrust.io/en/transfer?serial=K4QNAFCHDETK&return=https%3A%2F%2Fyour-site.example%2FpassportThe product named by serial is preselected when it belongs to the signed-in
visitor. A visitor who is not signed in goes through the sign-in page first,
then comes back to this same address, serial and return included. return
follows the same rule as for the claim. Two buttons bring the
visitor back: "Back to your-site.example" once the transfer is done, and "Back
to your-site.example without transferring" before.
- if you opened the window with
window.open, your page receives a message{ "type": "passport-transfer", "outcome": "transferred", "serial": "K4QNAFCHDETK" }addressed to the exact origin of thereturnpage, and the window closes; - otherwise, the visitor comes back to
return, withtransfer_outcome=transferredadded to the address.
outcome is transferred (transfer done), pending_acceptance (the recipient
still has to accept) or cancelled (the visitor came back without
transferring). Return and buyback are done from the owner's space on the hosted
page.
#Limits
The rate limit is counted per key and per brand, never per address: all the visitors your server represents do not share a single counter. It is described in the overview. These six endpoints do not consume the daily quota of the key.
#Errors
| Code | Condition | What to do |
|---|---|---|
| 400 | The visitor headers are missing. | Send the four headers described above. |
| 401 | Key missing or unknown, wrong signature, or time more than five minutes away from ours. | Sign each call at the moment you send it, with the exact path called. |
| 403 | The permission is missing, or the plan does not include API access. | Create a key that carries this permission. |
| 403 | SDM_MAC_MISMATCH: the chip's signature does not verify. | The read is not that of a genuine chip. |
| 404 | Unknown identifier, unpublished passport, expired receipt, or product of a brand your key does not serve. For a chip, the brand is checked before any replay check: a chip of another brand always answers 404, never 409, and its read is not consumed. | Check the identifier and the key. |
| 409 | This chip read was already verified elsewhere. | If our page verified it, read its receipt. |
| 422 | Invalid body, or a visitor address that is not public: a local address, or an address of a private network (10.x, 172.16 to 172.31, 192.168.x, 100.64 to 100.127, IPv6 starting with fc or fd). | Send the visitor's public address. Behind a load balancer, read it from the header the load balancer adds, not from the connection address. |
| 429 | Rate limit reached. | Wait for the delay given by Retry-After. |
| 503 | NFC_READ_IN_PROGRESS: your previous call is still verifying the same read. | Send the same read again after the delay given by Retry-After. |
#See also
- Partner API, overview, keys, permissions and limits.
- Fill the catalog through the API, the reseller key.
- API keys and webhooks, the setting in the console.
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.