TAGBASE

Custom domain

Serve verification on your own domain, fully white-label.

View as Markdown

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

  1. You point a subdomain at TAGBASE.
  2. TAGBASE checks the record and switches the domain on.
  3. Your tags get programmed with your address.
  4. A customer taps. TAGBASE checks the tag, then sends them to your page with the verification id in a tid parameter.
  5. 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.com and verify.brand.com count 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.com and brand.com are two different addresses. If they do not match, the browser blocks our answer and your code sees a network error.

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.