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.
tap → your app receives the scan → POST .../verifications → verdict
Fields
| Field | Type | Notes |
|---|---|---|
id |
string | vrf_-prefixed, assigned by the platform. |
status |
string | The verdict — see below. |
inserted_at |
string | ISO 8601 timestamp (UTC, microsecond precision). |
A verification also references its session and its tag 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.
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 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 — just the forwarded tap parameters,
no session relationship (to continue a session you’d add one alongside them):
{
"data": {
"type": "verifications",
"attributes": {
"...": "...scan parameters copied from the tap URL..."
}
}
}
To continue a session, add the session relationship:
{
"data": {
"type": "verifications",
"attributes": {
"...": "...scan parameters copied from the tap URL..."
},
"relationships": {
"session": { "data": { "type": "sessions", "id": "ses_abcdef0123456789" } }
}
}
}
curl https://platform.tagbase.io/api/v1/tags/tag_abcdef0123456789/verifications \
-X POST \
-H "Authorization: Bearer $TAGBASE_API_KEY" \
-H "Content-Type: application/vnd.api+json" \
-d '{ "data": { "type": "verifications", "attributes": { "...": "...tap URL params..." } } }'
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();
$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);
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
Response — 201 Created
{
"data": {
"type": "verifications",
"id": "vrf_abcdef0123456789",
"attributes": {
"status": "pending",
"inserted_at": "2026-06-08T12:34:56.123456Z"
},
"relationships": {
"session": { "data": { "type": "sessions", "id": "ses_abcdef0123456789" } },
"tag": { "data": { "type": "tags", "id": "tag_abcdef0123456789" } }
}
}
}
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
later.
Errors
| Status | When |
|---|---|
400 |
The body has no data.attributes object. |
401 |
Missing, invalid, or revoked key. |
404 |
No such tag under your account, 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
201withstatus: "invalid"— that’s a successful verification with a negative verdict.422means the request itself couldn’t be processed, not that the tag is fake.
Sessions and resolution
- A scan posted without a
sessionrelationship opens a new session: the verification comes backpendingwith asessionid. 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
sessionrelationship continues that session, and the verdict resolves tovalidorinvalid.
A session stays open for 10 minutes. 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
{
"data": [
{ "type": "verifications", "attributes": { "...": "...first tap params..." } },
{ "type": "verifications", "attributes": { "...": "...second tap params..." } }
]
}
curl https://platform.tagbase.io/api/v1/tags/tag_abcdef0123456789/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..." } }
] }'
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.
{
"data": [
{
"type": "verifications",
"id": "vrf_abcdef0123456789",
"attributes": { "status": "pending", "inserted_at": "2026-06-08T12:34:56.123456Z" },
"relationships": {
"session": { "data": { "type": "sessions", "id": "ses_abcdef0123456789" } },
"tag": { "data": { "type": "tags", "id": "tag_abcdef0123456789" } }
}
},
{
"type": "verifications",
"id": "vrf_bcdefa1234567890",
"attributes": { "status": "valid", "inserted_at": "2026-06-08T12:34:56.789012Z" },
"relationships": {
"session": { "data": { "type": "sessions", "id": "ses_abcdef0123456789" } },
"tag": { "data": { "type": "tags", "id": "tag_abcdef0123456789" } }
}
}
]
}
As with the single-scan call, persist each id and status on receipt; each is
also retrievable by id.
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
and tag relationships. The key you present must own the
underlying tag, or the platform responds 404.
curl https://platform.tagbase.io/api/v1/verifications/vrf_abcdef0123456789 \
-H "Authorization: Bearer $TAGBASE_API_KEY" \
-H "Accept: application/vnd.api+json"
const res = await fetch(
"https://platform.tagbase.io/api/v1/verifications/vrf_abcdef0123456789",
{
headers: {
"Authorization": `Bearer ${process.env.TAGBASE_API_KEY}`,
"Accept": "application/vnd.api+json",
},
},
);
const verification = await res.json();
$response = $client->get("https://platform.tagbase.io/api/v1/verifications/vrf_abcdef0123456789", [
"headers" => [
"Authorization" => "Bearer " . getenv("TAGBASE_API_KEY"),
"Accept" => "application/vnd.api+json",
],
]);
$verification = json_decode((string) $response->getBody(), true);
verification =
Req.get!("https://platform.tagbase.io/api/v1/verifications/vrf_abcdef0123456789",
headers: [
{"authorization", "Bearer #{System.fetch_env!("TAGBASE_API_KEY")}"},
{"accept", "application/vnd.api+json"}
]
).body
Response — 200 OK
{
"data": {
"type": "verifications",
"id": "vrf_abcdef0123456789",
"attributes": {
"status": "valid",
"inserted_at": "2026-06-08T12:34:56.123456Z"
},
"relationships": {
"session": { "data": { "type": "sessions", "id": "ses_abcdef0123456789" } },
"tag": { "data": { "type": "tags", "id": "tag_abcdef0123456789" } }
}
}
}
Errors
| Status | When |
|---|---|
401 |
Missing, invalid, or revoked key. |
404 |
No such verification under your account. |