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.

#Authorization

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.

NameTypeRequiredDescription
ustringyesThe 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.
tsintegeryesThe timestamp written into the address at the moment it was produced, in seconds since January 1, 1970.
sstringyesThe signature, as it appears in the address.
tstringnoThe identifier of the token. Old addresses omit it. The server finds the real identifier from u anyway, and that is the one it returns.
cstringnoThe 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.
srcstringnoPresent in the addresses of the old form, with the value qr. This endpoint does not declare it and takes no account of it.
latitudenumbernoLatitude 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.
longitudenumbernoLongitude 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"

#Example response

HTTP code 200, positive verdict.

JSON
{
  "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).

JSON
{
  "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.

JSON
{
  "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.

FieldTypeDescription
validbooleanThe verdict. It is the only field on which to build your logic.
messagestringA sentence in English that explains the verdict. Base your decisions on valid, never on this text.
uid_hashstring or nullThe fingerprint of the item, normalized: lowercase, 0x prefix. Filled in as soon as the item has been found, including on a negative verdict.
token_idstring or nullThe 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_namestring or nullThe name of the item. Same presence rule as token_id.
brand_namestring or nullThe name of the owning brand. Returned in the same cases as token_id, and null when the item is attached to no brand.
image_urlstring or nullThe 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_addressstring or nullThe address of the contract that carries this token, in lowercase. Same presence rule as token_id.
scan_areastring or nullThe 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_refstring or nullA 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

validmessageWhat it means
trueRegistered 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.
falseUnrecognised 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.
falseInvalid QR code signature. This QR code may be tampered with.The signature does not match the one we expect for this item.
falseQR 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.
falseThis product has not been minted yet.The item exists on our side, its minting has never been submitted. Calling back later will change nothing.
falseMint submitted, waiting for on-chain confirmation.The minting has been submitted and is waiting for its confirmation. Call back in a few minutes.
falseRegistered 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.
falseUnrecognised 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

CodeConditionWhat to do
404No 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.
404The 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.
404The 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.
422A 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.
429More 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.
500Unexpected 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.
503The 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

Your answer opens a pre-filled email in your mail app, addressed to contact@sealtrust.io. You read it over before sending it.

Suggest a correctionReport a problem