Your tags can carry your own web address. A customer taps and lands on
verify.brand.com. TAGBASE runs the check behind that name, and your brand is
the only one on screen.
Why use it
- Your name on the tag. Customers never see a TAGBASE address.
- Your page. You build the screen the customer lands on.
- The same check. Nothing about the security changes.
- One DNS record. Plus a few lines of code on your page.
Two addresses
This setup needs two web addresses, and they do different jobs.
| Example | Points at | Set in | |
|---|---|---|---|
| Custom domain | verify.brand.com |
TAGBASE | Settings |
| Redirect URL | www.brand.com/product |
Your own server | Each product |
The tag carries the custom domain. TAGBASE answers there, checks the tag, then sends the customer on to your redirect URL.
Pick a subdomain you use for nothing else. TAGBASE answers every request that reaches it. If you pick the one your website runs on, your website stops loading.
How it works
- You point a subdomain at TAGBASE.
- TAGBASE checks the record and switches the domain on.
- Your tags get programmed with your address.
- A customer taps. TAGBASE checks the tag, then sends them to your page with
the verification id in a
tidparameter. - Your page asks for the result and shows it.
Hand it to your coding agent
Copy this brief into Claude Code, Cursor, or whatever you build with. It walks the same steps as the setup below, in the same order, and your agent writes the page from it.
Add TAGBASE product verification to this site.
1. Before any code, point a subdomain of this site at TAGBASE. Add a CNAME
from verify.<domain> to external.tagbase.io, then confirm the domain
under Settings in the TAGBASE dashboard. Everything below asks that
subdomain for the answer, so stop here until it resolves.
2. Load the client with one script tag. There is nothing to install:
<script src="https://cdn.jsdelivr.net/npm/@tagbase-io/verify"></script>
It puts verify and VerifyError on window.tagbase.
3. Build the page the tag sends people to. Call
tagbase.verify({ baseUrl }), where baseUrl is the subdomain from step 1,
for example https://verify.example.com. The tag puts the id of the
result in the ?tid parameter and verify() reads it. Serve the page from
the same domain as that subdomain, and set the product's redirect URL to
the exact origin the page runs on.
4. Show what comes back: result.status, result.messages (the words for the
customer), result.title, result.description, result.imageUrls,
result.data and result.documents.
5. Write all four statuses. 'pending' asks for a second tap. 'valid' and
'valid_with_warnings' show the product. 'invalid' warns the customer
that the item may be a copy.
6. Read result.incognito before you render a status. verify() sets that
field on the same request. Use it. Do not add a detector, and do not
call anything before verify(). When incognito is true, tell the
customer to leave private mode and open this page in a normal window.
A pending status in that window is not a step they can finish.
7. Catch VerifyError. Its code is 'no_id', 'not_found', 'network',
'server' or 'location_required'. Write a page for a tag we have no
record of, and one that asks the customer to allow location access and
reload when the code is 'location_required'.
8. Keep the same check when the page reloads. The id is in ?tid. Leave it
in the address. On each load, call verify() once with that id. If you
store an id for this visit, reuse it only when the address has no tid.
A tid in the address replaces the stored id. When the page is already
loading, pending, valid, valid_with_warnings, or invalid, keep that
result. Do not start a second scan. Do not POST a verification.
verify() reads a check that already exists. A pending check goes stale
after ten minutes by default. Read result.insertedAt. Tell the customer
to tap the tag again. When the address then carries a new tid, drop the
stored id and show that new pending result. A valid,
valid_with_warnings, or invalid result stays. Reload shows it again.
Read https://verify.tagbase.io/docs/your-domain/external-verification
first. Match this brief, including step 8. The demo at
https://customdomain.xyz shows the private-mode screen, and its source is
at https://github.com/tagbase-io/customdomain.xyz.When that page is built, run the checklist.
Setup
1. Point your domain at TAGBASE
Add this record wherever your DNS lives:
| Type | Name | Value |
|---|---|---|
| CNAME | verify |
external.tagbase.io |
verify.brand.com is now a TAGBASE address under a name of yours. Confirm the
domain under Settings, then wait for it to go live. Nothing below works
until it does.
2. Load the client
Load it from a CDN. There is nothing to install. The script tag defines
tagbase, holding verify and VerifyError:
<script src="https://cdn.jsdelivr.net/npm/@tagbase-io/verify"></script>The URL follows the current release. Add @0.2.0 to it to pin a version.
The client is @tagbase-io/verify
on npm, with the types and the version history.
3. Read the result
Call verify on the page your tags redirect to. It reads the id out of the
address and asks your custom domain for the result:
const { verify } = tagbase
const result = await verify({ baseUrl: "https://verify.brand.com" })
if (result.status === "pending") {
// result.messages[0].text: "Please tap again to complete the verification process"
}baseUrl is the address from step 1, the one whose CNAME points at
external.tagbase.io. It is not the address your page runs on. The page reads
the id from ?tid=vrf_… in its own address, so ?tid= plus a real id is how
you open the page by hand. Pass id yourself if your router strips the
parameter.
Two rules about where your page lives. Break either one and the check stops working, and the error you get says nothing about why.
- Put your page on the same website. TAGBASE saves a cookie on the first
tap and reads it back on the second. Browsers hand that cookie back only to
the same website.
www.brand.comandverify.brand.comcount as the same website. Host it anywhere else and the second tap fails. - Give the product your page’s exact address. That is the redirect URL on
the product. Write it exactly as you serve the page.
www.brand.comandbrand.comare two different addresses. If they do not match, the browser blocks our answer and your code sees anetworkerror.
4. What comes back
| Field | Description |
|---|---|
id |
The verification, vrf_… |
status |
pending, valid, valid_with_warnings, or invalid |
messages |
What to show the customer, in the order they should read it |
title, description |
The product behind this tag |
imageUrls |
Product images and video |
documents |
Files to offer, each with a downloadUrl |
website |
The product’s own page, when the product has one |
data |
The tag’s custom data: category fields and raw JSON alike |
tamperStatus |
sealed, resealed, open, invalid, or null |
onDevice |
Whether this browser made the scan |
incognito |
Whether the customer browses privately |
insertedAt |
When the tag was tapped |
The first tap always reads pending. The second resolves it to valid or
invalid. valid_with_warnings means the product is real and something is
worth saying, so read messages for what. See
how it works.
messages arrive in English whatever language your customer reads. Translate
them yourself if you serve other languages.
5. Handle the failures
Every failure throws a VerifyError carrying a code. An unknown tag and an
unreachable server are different problems, so tell them apart before you write
an error message:
const { verify, VerifyError } = tagbase
try {
const result = await verify({ baseUrl: "https://verify.brand.com" })
} catch (error) {
if (error instanceof VerifyError && error.code === "not_found") {
// No record for this tag yet. Ask the customer to tap again.
}
}The codes are no_id, not_found, network, server, and
location_required. The last one only happens on a product that
requires a location: the customer refused the browser’s prompt.
Show them how to allow location access and call verify() again.
6. Private browsing
The check needs the first tap and the second to find each other. Private
browsing keeps them apart. verify() sets result.incognito on the same
request. Read that field. Serve your page over HTTPS, or the field is
unreliable.
A private window never gets past pending. Render that status there and you
ask the customer to keep tapping a tag that will never resolve. Give them the
reason instead:
if (result.incognito) {
// Tell the customer to leave private mode and open this page in a normal window.
} else {
// Render result.status.
}7. Location
Where a tag was tapped is part of every scan. How the client asks for it depends on the location requirement of your team or the product.
Optional
The client asks the browser where it is and reports the answer against the scan, so you can see where a tag was tapped. Your customer sees a permission prompt. If they refuse, the result still arrives without a location.
By default, TAGBASE decides itself when to trigger the browser’s location request, so the customer only sees the prompt when the answer is useful.
Pass geolocation: false to switch it off. The prompt never appears:
await verify({ baseUrl: "https://verify.brand.com", geolocation: false })Required
When the location requirement is Required, the server keeps the result
until the scanning browser has shared a location. The client handles this on
its own: it asks the browser, sends the answer, and reads the result, whatever
geolocation says. A customer who refuses gets a VerifyError with the code
location_required instead of a result, see
handle the failures. Our
location access page has the steps for
Safari and Chrome.
This needs the client at version 0.3 or newer. Older versions report the held result as a server error.
8. Keep the same check
The id in ?tid is the check. Leave it in the address. On a reload, call
verify() once with that id.
Store an id for this visit only as a fallback. Use the stored id when the
address has no tid. When the address has a tid, use that id and replace
the stored one.
Keep a result that is already on screen. That covers loading, pending,
valid, valid_with_warnings, and invalid. Do not start a second scan
for a result you already have.
This page reads a check that already exists. verify() does that read.
Do not POST a verification from the page.
A pending check times out after ten minutes by default. See
scanning a tag. Read
result.insertedAt. Ask the customer to tap the tag again. The next tap
opens a new pending check and a new id. Drop the stored id and show
that check. A valid, valid_with_warnings, or invalid result stays
on screen. A reload shows that same result.
The old response format
Requesting accept: application/json from https://verify.brand.com/verifications/{id}
returns an older flat {data, messages} body. This format is deprecated and
stops working on 2026-12-31.
Move to the client.
Best practices
When the page from the brief above is in place, paste this into the same agent. It checks the page and fixes what fails.
The verification page is built. Check it, then fix what fails.
1. Private mode. The page reads result.incognito from the verify() result.
A private window shows a message that tells the customer to leave
private mode. Pending in that window is not a step they can finish.
There is no call before verify(), and no second detector.
2. Reload while loading. Reload during the request. The page keeps the
same tid. It does not open a second check.
3. Reload while pending. Reload on the pending screen. The same pending
result comes back. The page does not start a new scan.
4. No duplicate POST. The page calls verify() and does not POST a
verification.
5. Reuse. A reload uses the tid in the address. A stored id is used only
when the address has no tid. A tid in the address replaces a stored id.
6. Stale pending. When result.insertedAt is older than ten minutes by
default and the status is still pending, tell the customer to tap the
tag again. When a new tid arrives, drop the old id and show the new
pending result.
7. Finished screens. Reload on valid, on valid_with_warnings, and on
invalid. Each screen comes back as it was. The page does not replace
a finished result with a new check.
Read https://verify.tagbase.io/docs/your-domain/external-verification
first. The demo at https://customdomain.xyz shows the private-mode
screen, and its source is at https://github.com/tagbase-io/customdomain.xyz.Stuck on the setup? Ask the agent for a human, or mail us. The Support page has both.