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 shows. It is read only; scans come from phones.
Every request needs an API key. 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 withfilter[status]=invalidand 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 readmeta.total_count, or group the rows you pulled by theproductrelationship.
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. |
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 and 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
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 leave those scans out, so a count from this API can be higher than the statistics show.
List verifications
GET /api/verificationsLists the verifications of the team, newest first. Paging follows the shared pagination rules.
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.
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"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();$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);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"}
]
).bodyResponse: 200 OK
A data array of verifications, each in the shape of
Retrieve a verification, plus a meta object
that describes the page:
{
"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/:idcurl https://verify.tagbase.io/api/verifications/vrf_TZB4HkNVeLVSLEgoYWkbC \
-H "Authorization: Bearer $TAGBASE_API_KEY" \
-H "Accept: application/vnd.api+json"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();$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);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"}
]
).bodyResponse: 200 OK
{
"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
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 and Products
endpoints read the records the tag and product relationships point at.