Method GET/qr /verify
Verify an item from the signed parameters carried by a verification address of the old form. Public endpoint, no API key.
On this page
You get an authenticity verdict for an item, from the signed parameters of a verification address, without any API key.
The full address is https://api.sealtrust.io/v1/qr/verify. The same route
exists without the /v1 prefix, and the /v1 form is the one recommended for
a new integration.
This endpoint replies 200 even when the verdict is negative. A tampered QR
code, a QR code that is too old, an item not yet minted: in all three cases the
response is a 200 whose valid field is false. The 4xx and 5xx codes are
reserved for the cases where no verdict can be returned.
#
None, public endpoint. It expects no API key, no session cookie, and no
Authorization header.
Call it from your server. Our cross-origin sharing policy allows only the
public site and the SealTrust console. From a web page hosted on another
domain, the browser blocks the response, and the X-RateLimit-Remaining header
is never exposed to a page script anyway.
What is authoritative here is the s value. It is the signature that we
affixed when producing the verification address. Without it, or with a modified
value, the verdict is negative.
#Call limit
30 calls per 60-second window, counted per calling IP address. The limit is
shared by all the addresses that start with /qr, and it applies to
/qr/verify as well as to /v1/qr/verify.
The responses carry the X-RateLimit-Limit, X-RateLimit-Remaining and
X-RateLimit-Reset headers, the last one giving the reset time in seconds
since January 1, 1970. Going over returns 429 with Retry-After in addition,
in seconds.
Do not make your code depend on the presence of these headers: read them if they are there, and carry on if they are missing.
This endpoint consumes no quota of your plan.
#Path and query parameters
This endpoint has no path parameter.
| Name | Type | Required | Description |
|---|---|---|---|
u | string | yes | The fingerprint of the item: 0x followed by 64 hexadecimal characters. Edge spaces are removed, case does not matter, and the 0x prefix is added if it is missing. |
ts | integer | yes | The timestamp written into the address at the moment it was produced, in seconds since January 1, 1970. |
s | string | yes | The signature, as it appears in the address. |
t | string | no | The identifier of the token. Old addresses omit it. The server finds the real identifier from u anyway, and that is the one it returns. |
c | string | no | The short reference of the contract carried by the address. This endpoint accepts it and never reads it. The presence or the absence of c changes nothing in the response. |
src | string | no | Present in the addresses of the old form, with the value qr. This endpoint does not declare it and takes no account of it. |
latitude | number | no | Latitude transmitted by the scanning device, between -90 and 90. Rounded to two decimal places, that is cells of about 1.1 km, before any recording. The fine position exists at no moment on our side. |
longitude | number | no | Longitude transmitted by the scanning device, between -180 and 180. Rounded the same way. |
latitude and longitude go in pairs. If you send only one of them, it is
discarded and the scan is recorded without coordinates.
The response never returns coordinates: at best an area label. The coordinates
you send are rounded to 2 decimal places (about 1 km) as soon as they are
received, and the label is then the town nearest to that point, found in a
table of towns read on our side. When you send no coordinates, the city and the
country are deduced from the calling IP address, through a lookup database read
on our side; the address of a hosting provider or a data centre gets none. Your
IP address is sent to no outside service. If the position of the scan matters
to you, send latitude and longitude. These coordinates are not covered by
the signature: a grey-market alert never counts them as a GPS position, but as
a network location (country_source is client). Otherwise, know that the
recorded area will be that of the calling machine's network.
#Request body
None. This is a GET request, everything goes through the query parameters.
#Example request
curl -i -G https://api.sealtrust.io/v1/qr/verify \
--data-urlencode "u=0x0000000000000000000000000000000000000000000000000000000000000000" \
--data-urlencode "t=1024" \
--data-urlencode "ts=1755000000" \
--data-urlencode "c=0x00000000" \
--data-urlencode "s=0000000000000000000000000000000000000000000000000000000000000000"// To be run on your server, with Node 18 or newer.
// From a web page on another domain, the browser blocks the response.
const parametres = new URLSearchParams({
u: "0x0000000000000000000000000000000000000000000000000000000000000000",
t: "1024",
ts: "1755000000",
c: "0x00000000",
s: "0000000000000000000000000000000000000000000000000000000000000000",
});
const response = await fetch(
`https://api.sealtrust.io/v1/qr/verify?${parametres.toString()}`,
);
console.log(response.status);
// This header is readable only from a server call, and it can be missing.
console.log(response.headers.get("X-RateLimit-Remaining"));
console.log(await response.json());import requests
response = requests.get(
"https://api.sealtrust.io/v1/qr/verify",
params={
"u": "0x0000000000000000000000000000000000000000000000000000000000000000",
"t": "1024",
"ts": "1755000000",
"c": "0x00000000",
"s": "0000000000000000000000000000000000000000000000000000000000000000",
},
timeout=30,
)
print(response.status_code)
# This header can be missing, so read it without requiring its presence.
print(response.headers.get("X-RateLimit-Remaining"))
print(response.json())#Example response
HTTP code 200, positive verdict.
{
"valid": true,
"message": "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.",
"uid_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"token_id": "1024",
"product_name": "Sac de voyage Exemple SAS",
"brand_name": "Exemple SAS",
"image_url": null,
"contract_address": "0x0000000000000000000000000000000000000000",
"scan_area": "Lyon, FR",
"scan_ref": "dGhpcy1pcy1hbi1leGFtcGxlLXJlZg"
}Here the item has no cover image, so image_url is null. When it has one,
the field carries its public address. The scan_area field is filled in
because the call could be located. It is null otherwise.
HTTP code 200 as well when the verdict is negative, and the response does not have the same shape depending on the verdict.
Three negative verdicts are returned before any read on the chain: signature
refused, address too old, item not yet minted. These three responses carry only
uid_hash, like the two negative verdicts specific to a piece in anchored mode
(see the table below).
{
"valid": false,
"message": "Invalid QR code signature. This QR code may be tampered with.",
"uid_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"token_id": null,
"product_name": null,
"brand_name": null,
"image_url": null,
"contract_address": null,
"scan_area": null
}The fourth negative verdict is returned after the read on the chain, when the
fingerprint read does not match. This response describes the item, like the
positive verdict. Only scan_area stays null, because the area is computed
only on a positive verdict.
{
"valid": false,
"message": "Unrecognised code: the QR signature or the on-chain record does not match.",
"uid_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"token_id": "1024",
"product_name": "Sac de voyage Exemple SAS",
"brand_name": "Exemple SAS",
"image_url": null,
"contract_address": "0x0000000000000000000000000000000000000000",
"scan_area": null
}The ten fields of the response.
| Field | Type | Description |
|---|---|---|
valid | boolean | The verdict. It is the only field on which to build your logic. |
message | string | A sentence in English that explains the verdict. Base your decisions on valid, never on this text. |
uid_hash | string or null | The fingerprint of the item, normalized: lowercase, 0x prefix. Filled in as soon as the item has been found, including on a negative verdict. |
token_id | string or null | The identifier of the token, in base 10, in the form of a string. It is the one recorded on our side, which can differ from the t that you sent. Filled in on a positive verdict and on the Unrecognised code: the QR signature or the on-chain record does not match. verdict. null for a piece in anchored mode that does not have its token yet: it is verified by the proof of its lot. |
product_name | string or null | The name of the item. Same presence rule as token_id. |
brand_name | string or null | The name of the owning brand. Returned in the same cases as token_id, and null when the item is attached to no brand. |
image_url | string or null | The public address of the cover image. null when the item and its model have none. It is the light copy of the photo (WebP, 800 pixels at most on the long side, 100 KB at most, no EXIF data) when it exists, else the original. An address of another site is returned as given. |
contract_address | string or null | The address of the contract that carries this token, in lowercase. Same presence rule as token_id. |
scan_area | string or null | The area of the scan. City, CC when the city and the two-letter country code are both known, for example Lyon, FR. When only one part is known, the field carries only that one, Lyon or FR. null when neither of the two is known. Filled in only on a positive verdict. Never coordinates, never a postal address. With coordinates, the city is the one nearest to that point in the GeoNames table of towns; without coordinates, it comes from the IP address, looked up in the DB-IP Lite database. If you display it, add the link its source's CC BY 4.0 licence requires: GeoNames (nouvel onglet) or IP Geolocation by DB-IP (nouvel onglet). |
scan_ref | string or null | A one-time reference to this scan, valid for 30 minutes, filled in only on a positive verdict. Our verification page uses it to attach a more precise location when the person scanning explicitly agrees. You can ignore it. |
#The eight possible verdicts
valid | message | What it means |
|---|---|---|
true | 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. | The signature is recognized and the fingerprint recorded on the chain matches. |
false | Unrecognised code: the QR signature or the on-chain record does not match. | The signature is recognized, but the fingerprint read on the chain does not match. |
false | Invalid QR code signature. This QR code may be tampered with. | The signature does not match the one we expect for this item. |
false | QR code has expired. Please request a new QR code. | The ts timestamp differs from the current time by more than 30 days, ahead as well as behind. Produce a new verification address from your console. |
false | This product has not been minted yet. | The item exists on our side, its minting has never been submitted. Calling back later will change nothing. |
false | Mint submitted, waiting for on-chain confirmation. | The minting has been submitted and is waiting for its confirmation. Call back in a few minutes. |
false | Registered number: its record on the blockchain is not confirmed yet. | A piece in anchored mode, without a token: the record of its lot is not confirmed yet. Call back later. |
false | Unrecognised code: the on-chain record does not match. | A piece in anchored mode, without a token: the record of its lot was refused, its proof no longer reaches the recorded root, the proof does not name this piece (serial number, fingerprint, brand), or the contract does not keep that root. |
A piece in anchored mode that does not have its token yet is verified by the
proof of its lot, not by a token: the positive verdict above is returned to it
once the record of its lot is confirmed and its proof checked, with
token_id set to null. The root is read on the contract, once per lot,
and that result is kept when it matches; if the chain cannot be read, the
response is a 503 and no verdict is returned. This verdict counts as a scan, as for any piece, but
is not recorded as a token verification. See
Pieces registered by lot.
#Errors
| Code | Condition | What to do |
|---|---|---|
| 404 | No live item carries this fingerprint. Message Product not found for this QR code. The item has been destroyed, or withdrawn from the catalog. | Check the value of u. If the item has been destroyed or withdrawn from the catalog, this code is final. |
| 404 | The chain does not know this token. Message Unknown product: this token_id does not exist. | Nothing to correct on the call side. Contact the brand: the item is recorded on our side with a token identifier that the contract does not carry. |
| 404 | The call arrives on a brand's verified domain name, and the product belongs to another brand. detail is Product not found for this QR code. | Call this address on the domain of the brand concerned, or on api.sealtrust.io. The message is the same as for an unknown identifier, so as not to confirm that it exists. |
| 422 | A required parameter is missing (u, ts or s), ts is not an integer, or latitude and longitude are out of their bounds. | The body of the response lists the parameters at fault and the reason for each refusal. Correct and call again. |
| 429 | More than 30 calls in 60 seconds from the same IP address. Message Rate limit exceeded: 30 requests per 60s. | Wait the number of seconds given by Retry-After. Spread out your calls instead of sending them in bursts. |
| 500 | Unexpected error on the server. Fixed body {"detail": "Internal Server Error"}. | Try again. The X-Request-Id header identifies the call; send it to us if it repeats. |
| 503 | The read on the chain failed. Message Error during blockchain verification. | Try again in a few moments. No verdict was returned, and the scan was not recorded. |
#See also
GET /p/{serial}, translate the printed serial number into the address of a consumer page.GET /resolve/{identifier}, read in one call everything a product page displays.- Physical identification, QR and NFC, choose the physical carrier and the exact form of the GS1 Digital Link.
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.