# GET /qr/verify

Verify an item from the signed parameters carried by a verification address of the old form. Public endpoint, no API key.

Source: https://docs.sealtrust.io/en/reference/get-qr-verify/

---

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.

> [!ATTENTION] The QR code printed today does not feed this endpoint
> The QR code that we produce for an item encodes the short address
> `https://sealtrust.io/p/{serial number}`. It carries no signed parameter. So
> you cannot copy anything from a recent SealTrust QR code into this endpoint.
> To read a recent QR code, see the page
> `GET /p/{serial}`.
>
> This endpoint serves the verification addresses of the old form,
> `https://sealtrust.io/verify?u=…&t=…&ts=…&c=…&s=…&src=qr`. It carries six
> parameters. This endpoint reads five of them and does not declare `src`.

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.

> [!ATTENTION] Every accepted call leaves a trace
> A call whose signature is recognized and whose item is minted is recorded as
> a verification. If the verdict is positive, it counts as a scan on top of
> that: it can activate the item, feed your distribution statistics and trigger
> the `product.scanned` notification if your brand is subscribed to it. The
> scan also records a city and a country, deduced from the calling IP address
> when the call sends no coordinates. A call launched from your server
> therefore writes the position of that server into the distribution statistics
> of the brand. Do not use this endpoint as an availability probe.

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

| 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. When you send no
coordinates, the city and the country of that label are deduced from the
calling IP address, through a lookup database read on our side. Your IP address
is sent to no outside service. If the position of the scan matters to you, send
`latitude` and `longitude`. Otherwise, know that the recorded area will be that
of the calling machine.

## Request body

None. This is a `GET` request, everything goes through the query parameters.

## Example request

:::onglets
```bash title="curl"
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"
```
```typescript
// 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());
```
```python
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())
```
:::

> [!INFO] This page is an exception to the tabs rule
> Elsewhere on this site, the TypeScript tab goes through the
> `@sealtrust-io/sdk` package. Here it calls the API directly, because the SDK
> does not cover verification by QR code. See the TypeScript SDK page for the
> list of what it covers.

## Example response

HTTP code 200, positive verdict.

```json
{
  "valid": true,
  "message": "Authentic product: QR code and blockchain verified.",
  "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"
}
```

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

```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": "Invalid product: blockchain verification failed.",
  "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 nine 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 `Invalid product: blockchain verification failed.` verdict |
| `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. |
| `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. |

### The six possible verdicts

| `valid` | `message` | What it means |
| --- | --- | --- |
| `true` | `Authentic product: QR code and blockchain verified.` | The signature is recognized and the fingerprint recorded on the chain matches. |
| `false` | `Invalid product: blockchain verification failed.` | 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. |

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

> [!INFO] A new minting does not break an address already printed
> The signature bears on the stable identity of the item, without the token
> number or the address of the contract. If an item is re-minted, the old row
> is marked as replaced and the server replies with the most recent row. The
> same verification address keeps returning a verdict, with the new `token_id`
> and the new `contract_address`.

> [!INFO] The order of the checks
> The checks follow one another in this order: call limit, validation of the
> parameters, lookup of the item, signature, age of the address, minting state
> of the item, then read on the chain. A 404 means that the fingerprint sent
> matches no item: check the value of `u`.

## See also

- [`GET /p/{serial}`](/en/reference/get-p-serial/),
  translate the printed serial number into the address of a consumer page.
- [`GET /resolve/{identifier}`](/en/reference/get-resolve/),
  read in one call everything a product page displays.
- [Physical identification, QR and NFC](/en/identification-physique/),
  choose the physical carrier and the exact form of the GS1 Digital Link.
