# Verifications

Every scan of your tags, with its status and the tag behind it.


A **verification** is one scan of one of your tags. Each carries the outcome
of the check, the state of the tamper seal, the country the scan came from,
and the tag and product behind it. This API reads the same list the
dashboard's [verifications page](/docs/dashboard/verifications) shows. It is
read only; scans come from phones.

Every request needs an [API key](/docs/api/authentication). A key is limited
to the verifications of its own team.

## Use cases

- **Pull scans into a BI tool.** Fetch the list once a night with
  `filter[from]` set to yesterday, and load the rows into your warehouse next
  to your sales data.
- **Alert on `invalid`.** Poll with `filter[status]=invalid` and page a
  colleague when a new row appears. An invalid scan is a copied tag or a
  broken one, and either is worth a look the same day.
- **Count scans per product.** Request `filter[product_id]` for each product
  and read `meta.total_count`, or group the rows you pulled by the `product`
  relationship.

## Fields

A verification is a JSON:API resource of type `verifications`.

| Field | Type | Notes |
| --- | --- | --- |
| `id` | string | `vrf_`-prefixed. The same id the dashboard's verification URLs carry. |
| `status` | string | `pending`, `valid`, or `invalid`. See below. |
| `tamper_status` | string or `null` | `sealed`, `resealed`, `open`, or `invalid`. `null` when the tag has no [seal](/docs/dashboard/verifications#the-seal). |
| `country` | string or `null` | Where the scan came from, as an ISO 3166-1 alpha-2 code such as `AT`, read from the phone's IP address. `null` when the address gave no country. |
| `inserted_at` | string | When the tag was tapped, as an ISO 8601 timestamp in UTC. |

The `tag` relationship names the tag that was scanned,
`{"data": {"type": "tags", "id": "tag_…"}}`, and the `product` relationship
names its product, `{"data": {"type": "products", "id": "prd_…"}}`. Both ids
match the [Tags](/docs/api/tags) and [Products](/docs/api/products)
endpoints, so you can join scans to the records you already hold.

### Statuses match the dashboard

`pending` is the first tap, which the dashboard lists as **Scanned**; the
verification stays open until the second tap that proves the tag is live.
`valid` is a second tap that confirmed a genuine tag. `invalid` is a scan that
failed the check, for example a copied tag replaying an old tap. The
dashboard's
[what the statuses mean](/docs/dashboard/verifications#what-the-statuses-mean)
has the full description.

### One row per scan session

When a second tap follows a first tap within the session window, the list
shows one row with the final status. When no second tap follows, the row
shows as `pending`.

### Team members' own scans

When a signed-in member of your team taps a tag, the scan is in this list,
the same as on the dashboard's verifications page. The dashboard
[statistics](/docs/dashboard/statistics) leave those scans out, so a count
from this API can be higher than the statistics show.

## List verifications

```
GET /api/verifications
```

Lists the verifications of the team, newest first. Paging follows the shared
[pagination rules](/docs/api/introduction#pagination).

### Query parameters

| Parameter | Type | Notes |
| --- | --- | --- |
| `filter[status]` | string | Only scans with this status: `pending`, `valid`, or `invalid`. |
| `filter[product_id]` | string | Only scans of this product, `prd_…`. |
| `filter[from]` | date | Only scans on or after this day, `YYYY-MM-DD`, in UTC. |
| `filter[to]` | date | Only scans on or before this day, `YYYY-MM-DD`, in UTC. |
| `page` | integer | The page, counting from 1. Defaults to `1`. |
| `page_size` | integer | Verifications per page. Defaults to `10`, maximum `100`. |

Both dates are inclusive, so `filter[from]=2026-09-01&filter[to]=2026-09-30`
covers the whole of September. The API ignores a date it cannot parse.

<!-- tabs -->
```bash cURL
curl "https://verify.tagbase.io/api/verifications?filter[status]=valid&filter[from]=2026-09-01&filter[to]=2026-09-30&page_size=100" \
  -H "Authorization: Bearer $TAGBASE_API_KEY" \
  -H "Accept: application/vnd.api+json"
```
```js
const res = await fetch(
  "https://verify.tagbase.io/api/verifications?filter[status]=valid&filter[from]=2026-09-01&filter[to]=2026-09-30&page_size=100",
  {
    headers: {
      "Authorization": `Bearer ${process.env.TAGBASE_API_KEY}`,
      "Accept": "application/vnd.api+json",
    },
  },
);
const { data, meta } = await res.json();
```
```php
$response = $client->get("https://verify.tagbase.io/api/verifications", [
    "headers" => [
        "Authorization" => "Bearer " . getenv("TAGBASE_API_KEY"),
        "Accept"        => "application/vnd.api+json",
    ],
    "query" => [
        "filter"    => ["status" => "valid", "from" => "2026-09-01", "to" => "2026-09-30"],
        "page_size" => 100,
    ],
]);
$page = json_decode((string) $response->getBody(), true);
```
```elixir
page =
  Req.get!("https://verify.tagbase.io/api/verifications",
    params: [
      "filter[status]": "valid",
      "filter[from]": "2026-09-01",
      "filter[to]": "2026-09-30",
      page_size: 100
    ],
    headers: [
      {"authorization", "Bearer #{System.fetch_env!("TAGBASE_API_KEY")}"},
      {"accept", "application/vnd.api+json"}
    ]
  ).body
```
<!-- /tabs -->

### Response: `200 OK`

A `data` array of verifications, each in the shape of
[Retrieve a verification](#retrieve-a-verification), plus a `meta` object
that describes the page:

```json
{
  "data": [
    {
      "type": "verifications",
      "id": "vrf_TZB4HkNVeLVSLEgoYWkbC",
      "attributes": {
        "status": "valid",
        "tamper_status": "sealed",
        "country": "AT",
        "inserted_at": "2026-09-09T13:44:46.140127Z"
      },
      "relationships": {
        "tag": { "data": { "type": "tags", "id": "tag_TKWk9WD8BiXrurBwjeZ5NW" } },
        "product": { "data": { "type": "products", "id": "prd_4fm3xUcsDEUK77WDBMed6U" } }
      }
    }
  ],
  "meta": { "total_count": 1, "page": 1, "page_size": 100 }
}
```

`meta.total_count` counts the rows that match your filters, so a request with
`filter[product_id]` and `page_size=1` is a cheap way to count the scans of
one product.

### Errors

| Status | When |
| --- | --- |
| `401` | Missing, invalid, or revoked key. |

## Retrieve a verification

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

<!-- tabs -->
```bash cURL
curl https://verify.tagbase.io/api/verifications/vrf_TZB4HkNVeLVSLEgoYWkbC \
  -H "Authorization: Bearer $TAGBASE_API_KEY" \
  -H "Accept: application/vnd.api+json"
```
```js
const res = await fetch(
  "https://verify.tagbase.io/api/verifications/vrf_TZB4HkNVeLVSLEgoYWkbC",
  {
    headers: {
      "Authorization": `Bearer ${process.env.TAGBASE_API_KEY}`,
      "Accept": "application/vnd.api+json",
    },
  },
);
const verification = await res.json();
```
```php
$response = $client->get("https://verify.tagbase.io/api/verifications/vrf_TZB4HkNVeLVSLEgoYWkbC", [
    "headers" => [
        "Authorization" => "Bearer " . getenv("TAGBASE_API_KEY"),
        "Accept"        => "application/vnd.api+json",
    ],
]);
$verification = json_decode((string) $response->getBody(), true);
```
```elixir
verification =
  Req.get!("https://verify.tagbase.io/api/verifications/vrf_TZB4HkNVeLVSLEgoYWkbC",
    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_TZB4HkNVeLVSLEgoYWkbC",
    "attributes": {
      "status": "valid",
      "tamper_status": "sealed",
      "country": "AT",
      "inserted_at": "2026-09-09T13:44:46.140127Z"
    },
    "relationships": {
      "tag": { "data": { "type": "tags", "id": "tag_TKWk9WD8BiXrurBwjeZ5NW" } },
      "product": { "data": { "type": "products", "id": "prd_4fm3xUcsDEUK77WDBMed6U" } }
    }
  }
}
```

### Errors

| Status | When |
| --- | --- |
| `401` | Missing, invalid, or revoked key. |
| `404` | No such verification in your team. A verification of another team and a malformed id get the same answer. |

**The same URL also answers without a key.** Without an `Authorization` header,
`GET /api/verifications/:id` is the endpoint the browser client on
[your own domain](/docs/your-domain/external-verification#4-what-comes-back)
calls after a scan. It answers in the customer's vocabulary, with
`valid_with_warnings` and `messages`, and it shows a first tap only to the
phone that made it. Send the key and you get the team shape described here
instead. A wrong or revoked key answers `401`; the API does not fall back to
the public answer.

Next: the [Tags](/docs/api/tags) and [Products](/docs/api/products)
endpoints read the records the `tag` and `product` relationships point at.

