# Verifications

Submit a scan and read back the authenticity verdict.


A **verification** is the platform's verdict for a scan. When someone taps a tag,
your solution forwards the scan to the platform and receives a verification that
says whether the tag is genuine. This is the core call your integration makes.

## How scanning works

Your solution owns the scan entry point. A tag's chip is programmed with a URL
that points at *your* app (`https://<your-entry-point>/<tag_id>?<scan params>`),
so a tap arrives as a request to you, with the tag id in the path and the scan
parameters in the query string. You then hand the scan to the platform, which
validates it and records the verification.

[Image: A tap reaches your app, your app posts the scan to the verifications endpoint, and the platform answers with a verdict.. See the web version of this page.]

## Fields

| Field           | Type   | Notes                                              |
|-----------------|--------|----------------------------------------------------|
| `id`            | string | `vrf_`-prefixed, assigned by the platform.                 |
| `status`        | string | The verdict (see below).                           |
| `tamper_status` | string | The seal reading (see below). `null` on a tag without a seal. |
| `inserted_at`   | string | ISO 8601 timestamp (UTC, microsecond precision).   |

A verification also references its [session](/docs/api/sessions) and its
[tag](/docs/api/tags) as relationships.

### Status values

| Status            | Meaning                                                         |
|-------------------|-----------------------------------------------------------------|
| `pending`         | Scan accepted; the session hasn't resolved to a final verdict yet. |
| `valid`           | Genuine: the tag verified successfully.                        |
| `invalid`         | Failed: the scan didn't check out.                             |

A verification you create comes back as exactly one of these three.

### Seal readings

Some tags carry a **tamper seal**: a physical loop that the tag checks each time
it is tapped to see whether the product has been opened. Every scan of such a tag
returns the reading in `tamper_status`, alongside the verdict:

| Reading    | Meaning                                                              |
|------------|----------------------------------------------------------------------|
| `sealed`   | The seal is intact; the product has not been opened.                 |
| `resealed` | The seal was opened at some point and is now closed again.           |
| `open`     | The seal is broken; the product has been opened.                     |
| `invalid`  | The tag could not read its seal.                                     |

`tamper_status` is `null` for a tag without a seal. Order a tag with one and the
field starts carrying a reading; nothing else about your integration changes.

The seal is independent of `status`. A tag can verify as `valid` and still report
`open`, which describes a genuine product in opened packaging. Treat the two as
separate signals, and decide for yourself whether an opened seal changes what you
show the customer.

## Submit a scan

```
POST /api/v1/tags/:tag_id/verifications
```

`:tag_id` is the tag id of the scanned tag. The key you present must own that
tag, or the platform responds `404`.

This posts one scan at a time. If you already hold both scans of a tag, you can
[submit them together](#submit-both-scans-at-once) and resolve in a single call.

### Request

The `attributes` are **the tap URL's query string, parsed into key/value pairs:
every parameter, unchanged**. When a tag is tapped, its chip produces a URL whose
query string carries the data for that tap; your entry point receives it, and you
copy the whole parsed query string into `attributes`. You never name or interpret
those parameters: pass them through verbatim.

To continue a session an earlier verification returned, set the `session`
relationship to its id; omit `relationships` entirely to start a fresh one.

| Field                          | Required | Notes                                                |
|--------------------------------|----------|------------------------------------------------------|
| `attributes`                   | yes      | The scan parameters, copied from the tap URL's query string. |
| `relationships.session`        | no       | The session to continue. Omit to start a new one.    |

Here's a scan that starts a fresh session, with just the forwarded tap parameters
and no `session` relationship (to continue a session you'd add one alongside them):

```json
{
  "data": {
    "type": "verifications",
    "attributes": {
      "...": "...scan parameters copied from the tap URL..."
    }
  }
}
```

To continue a session, add the `session` relationship:

```json
{
  "data": {
    "type": "verifications",
    "attributes": {
      "...": "...scan parameters copied from the tap URL..."
    },
    "relationships": {
      "session": { "data": { "type": "sessions", "id": "ses_2cZuH8Cb7L5fseae6FzNDc" } }
    }
  }
}
```

<!-- tabs -->
```bash cURL
curl https://platform.tagbase.io/api/v1/tags/tag_XPg3Zq7gEExNezDWWW7Zzo/verifications \
  -X POST \
  -H "Authorization: Bearer $TAGBASE_API_KEY" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{ "data": { "type": "verifications", "attributes": { "...": "...tap URL params..." } } }'
```
```js
const res = await fetch(
  `https://platform.tagbase.io/api/v1/tags/${tagId}/verifications`,
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.TAGBASE_API_KEY}`,
      "Content-Type": "application/vnd.api+json",
    },
    body: JSON.stringify({ data: { type: "verifications", attributes } }),
  },
);
const verification = await res.json();
```
```php
$response = $client->post(
    "https://platform.tagbase.io/api/v1/tags/{$tagId}/verifications",
    [
        "headers" => [
            "Authorization" => "Bearer " . getenv("TAGBASE_API_KEY"),
            "Content-Type"  => "application/vnd.api+json",
        ],
        "json" => ["data" => ["type" => "verifications", "attributes" => $attributes]],
    ],
);
$verification = json_decode((string) $response->getBody(), true);
```
```elixir
verification =
  Req.post!("https://platform.tagbase.io/api/v1/tags/#{tag_id}/verifications",
    headers: [
      {"authorization", "Bearer #{System.fetch_env!("TAGBASE_API_KEY")}"},
      {"content-type", "application/vnd.api+json"}
    ],
    json: %{data: %{type: "verifications", attributes: attributes}}
  ).body
```
<!-- /tabs -->

### Response: `201 Created`

```json
{
  "data": {
    "type": "verifications",
    "id": "vrf_AoNAiMHGpUb2fxAB93eM4R",
    "attributes": {
      "status": "pending",
      "tamper_status": "sealed",
      "inserted_at": "2026-06-08T12:34:56.123456Z"
    },
    "relationships": {
      "session": { "data": { "type": "sessions", "id": "ses_2cZuH8Cb7L5fseae6FzNDc" } },
      "tag":     { "data": { "type": "tags",     "id": "tag_XPg3Zq7gEExNezDWWW7Zzo" } }
    }
  }
}
```

Persist `id`, `status`, and the `session` id on your side as soon as you receive
them; you can also [re-fetch a verification by id](#retrieve-a-verification)
later. Store `tamper_status` too if your tags carry a seal. The tag measures it
at tap time, so re-fetching a verification later returns the reading from that
tap rather than the state of the seal today.

### Errors

| Status | When                                                                       |
|--------|----------------------------------------------------------------------------|
| `400`  | The body has no `data.attributes` object.                                  |
| `401`  | Missing, invalid, or revoked key.                                          |
| `404`  | No such tag under your team, or the tag isn't written to a chip yet.    |
| `422`  | The scan couldn't be recorded; nothing was saved, so retrying the same request is safe. **Not** a failed security check. |

> Note that a tag that fails its security check still returns `201` with
> `status: "invalid"`. That's a successful verification with a negative verdict.
> `422` means the request itself couldn't be processed, not that the tag is fake.

## Sessions and resolution

- A scan posted **without** a `session` relationship opens a new session: the
  verification comes back `pending` with a `session` id. Store that id against
  the scanning visitor (their session or a cookie). Scans are linked only
  because you re-present this id, not by anything the platform tracks about the
  device.
- A scan posted **with** a `session` relationship continues that session, and the
  verdict resolves to `valid` or `invalid`.

A session stays open for the tag's `session_duration`: **10 minutes** unless
you [configured the tag](/docs/api/tags) otherwise. After that it expires; a
later scan referencing it starts a fresh `pending` session instead of resolving
the old one.

## Submit both scans at once

The flow above spans two requests glued by a session. If you already hold
**both** scans of a tag (for example a client that buffered taps while offline
and is now syncing), you can submit them together and get the final verdict in a
single call, with no `pending` step and no second round-trip.

Send `data` as an **array of exactly two** scans instead of a single object.
Each entry is a `verifications` resource whose `attributes` are one scan's tap
parameters, copied verbatim, the same pass-through as a single scan. Order
matters: the first entry is the first tap, the second is the second tap. You
don't set a `session` relationship; the call resolves on its own.

### Request

```json
{
  "data": [
    { "type": "verifications", "attributes": { "...": "...first tap params..." } },
    { "type": "verifications", "attributes": { "...": "...second tap params..." } }
  ]
}
```

<!-- tabs -->
```bash cURL
curl https://platform.tagbase.io/api/v1/tags/tag_XPg3Zq7gEExNezDWWW7Zzo/verifications \
  -X POST \
  -H "Authorization: Bearer $TAGBASE_API_KEY" \
  -H "Content-Type: application/vnd.api+json" \
  -d '{ "data": [
        { "type": "verifications", "attributes": { "...": "...first tap params..." } },
        { "type": "verifications", "attributes": { "...": "...second tap params..." } }
      ] }'
```
```js
const res = await fetch(
  `https://platform.tagbase.io/api/v1/tags/${tagId}/verifications`,
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.TAGBASE_API_KEY}`,
      "Content-Type": "application/vnd.api+json",
    },
    body: JSON.stringify({
      data: [
        { type: "verifications", attributes: firstTap },
        { type: "verifications", attributes: secondTap },
      ],
    }),
  },
);
const verifications = (await res.json()).data;
```
```php
$response = $client->post(
    "https://platform.tagbase.io/api/v1/tags/{$tagId}/verifications",
    [
        "headers" => [
            "Authorization" => "Bearer " . getenv("TAGBASE_API_KEY"),
            "Content-Type"  => "application/vnd.api+json",
        ],
        "json" => [
            "data" => [
                ["type" => "verifications", "attributes" => $firstTap],
                ["type" => "verifications", "attributes" => $secondTap],
            ],
        ],
    ],
);
$verifications = json_decode((string) $response->getBody(), true)["data"];
```
```elixir
verifications =
  Req.post!("https://platform.tagbase.io/api/v1/tags/#{tag_id}/verifications",
    headers: [
      {"authorization", "Bearer #{System.fetch_env!("TAGBASE_API_KEY")}"},
      {"content-type", "application/vnd.api+json"}
    ],
    json: %{
      data: [
        %{type: "verifications", attributes: first_tap},
        %{type: "verifications", attributes: second_tap}
      ]
    }
  ).body["data"]
```
<!-- /tabs -->

### Response: `201 Created`

You get a `data` **array** of the verification records the call created, in scan
order. The **last** entry carries the final verdict (`valid` or `invalid`).
Read your authenticity result from it.

```json
{
  "data": [
    {
      "type": "verifications",
      "id": "vrf_AoNAiMHGpUb2fxAB93eM4R",
      "attributes": { "status": "pending", "inserted_at": "2026-06-08T12:34:56.123456Z" },
      "relationships": {
        "session": { "data": { "type": "sessions", "id": "ses_2cZuH8Cb7L5fseae6FzNDc" } },
        "tag":     { "data": { "type": "tags",     "id": "tag_XPg3Zq7gEExNezDWWW7Zzo" } }
      }
    },
    {
      "type": "verifications",
      "id": "vrf_XBUGqbnP8BzQ7yJAmyrtaw",
      "attributes": { "status": "valid", "inserted_at": "2026-06-08T12:34:56.789012Z" },
      "relationships": {
        "session": { "data": { "type": "sessions", "id": "ses_2cZuH8Cb7L5fseae6FzNDc" } },
        "tag":     { "data": { "type": "tags",     "id": "tag_XPg3Zq7gEExNezDWWW7Zzo" } }
      }
    }
  ]
}
```

As with the single-scan call, persist each `id` and `status` on receipt; each is
also [retrievable by id](#retrieve-a-verification).

### Errors

Same as a single scan, plus one shape check:

| Status | When                                                                          |
|--------|-------------------------------------------------------------------------------|
| `400`  | `data` is an array but doesn't hold exactly two scans, or an entry is missing its `attributes`. |

## Retrieve a verification

```
GET /api/v1/verifications/:id
```

Fetch a verification by id: its `status` and its [session](/docs/api/sessions)
and [tag](/docs/api/tags) relationships. The key you present must own the
underlying tag, or the platform responds `404`.

<!-- tabs -->
```bash cURL
curl https://platform.tagbase.io/api/v1/verifications/vrf_AoNAiMHGpUb2fxAB93eM4R \
  -H "Authorization: Bearer $TAGBASE_API_KEY" \
  -H "Accept: application/vnd.api+json"
```
```js
const res = await fetch(
  "https://platform.tagbase.io/api/v1/verifications/vrf_AoNAiMHGpUb2fxAB93eM4R",
  {
    headers: {
      "Authorization": `Bearer ${process.env.TAGBASE_API_KEY}`,
      "Accept": "application/vnd.api+json",
    },
  },
);
const verification = await res.json();
```
```php
$response = $client->get("https://platform.tagbase.io/api/v1/verifications/vrf_AoNAiMHGpUb2fxAB93eM4R", [
    "headers" => [
        "Authorization" => "Bearer " . getenv("TAGBASE_API_KEY"),
        "Accept"        => "application/vnd.api+json",
    ],
]);
$verification = json_decode((string) $response->getBody(), true);
```
```elixir
verification =
  Req.get!("https://platform.tagbase.io/api/v1/verifications/vrf_AoNAiMHGpUb2fxAB93eM4R",
    headers: [
      {"authorization", "Bearer #{System.fetch_env!("TAGBASE_API_KEY")}"},
      {"accept", "application/vnd.api+json"}
    ]
  ).body
```
<!-- /tabs -->

### Response: `200 OK`

```json
{
  "data": {
    "type": "verifications",
    "id": "vrf_AoNAiMHGpUb2fxAB93eM4R",
    "attributes": {
      "status": "valid",
      "inserted_at": "2026-06-08T12:34:56.123456Z"
    },
    "relationships": {
      "session": { "data": { "type": "sessions", "id": "ses_2cZuH8Cb7L5fseae6FzNDc" } },
      "tag":     { "data": { "type": "tags",     "id": "tag_XPg3Zq7gEExNezDWWW7Zzo" } }
    }
  }
}
```

### Errors

| Status | When                                     |
|--------|------------------------------------------|
| `401`  | Missing, invalid, or revoked key.        |
| `404`  | No such verification under your team. |

