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.
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.
The one key you add yourself is tagbase_session_id: set it to a session id an
earlier verification returned to continue that session, or omit it to start a
fresh one. Scan parameters never use that name, so it won’t collide with the
parameters you’re forwarding.
| Attribute | Required | Notes |
|---|---|---|
| (tap URL params) | yes | The scan parameters, copied from the tap URL’s query string. |
tagbase_session_id |
no | Id of 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 tagbase_session_id (to continue a session you’d add that key alongside
them):
{
"data": {
"type": "verifications",
"attributes": {
"...": "...scan parameters copied from the tap URL..."
}
}
}
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 — there’s no endpoint to re-fetch a verification 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 — a rare transactional error; 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
tagbase_session_idopens 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
tagbase_session_idcontinues 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.