EdgeNFC
Sign in Start free

E-commerce / Shopify#

Send a genuine, counter-advanced tap to a destination you control — a Shopify product page, a claim page, or a rewards portal. The redirect target is owner-stored config, never taken from the tag or the URL, so a forgery can never reach attacker-controlled input: it falls back to the safe tap page and never redirects.

This is the same verification core as the Hosted Gateway — you are just adding a destination on top of it.

How it works#

  1. A customer taps a provisioned tag. The phone opens the tag's SUN URL at /t (the human tap path).
  2. The gateway verifies the tap: it re-derives the per-tag key, checks the MAC, and advances the monotonic counter.
  3. If the tap is authentic and you have set a destination, the gateway 302-redirects to it — with the tap context available to the target.
  4. If the tap is forged, replayed, or unknown, there is no redirect; the visitor sees the "Not verified" tap page. Your Shopify store never receives the attacker's request.

Because step 3 only fires on an authentic, counter-advanced tap, a redirect is a proof of genuineness for everything downstream of it.

Set the system destination (Creator+)#

Point the whole system at one destination with POST /api/systems/{id}/routing. Custom routing is a Creator+ feature; the entitlement is checked here, at config time, never on the hot tap path.

curl -X POST https://edgenfc.com/api/systems/sys_01H…/routing \
  -H "authorization: Bearer $TOKEN" \
  -H "content-type: application/json" \
  -d '{ "url": "https://shop.example.com/products/limited-drop" }'

Response:

{ "system_id": "sys_01H…", "route_url": "https://shop.example.com/products/limited-drop" }

Now every authentic tap on that system 302s to the Shopify product. The url must be an absolute http(s) URL — anything else is rejected 400 and never stored, so a bad value can never become a redirect target. Send { "url": null } to clear the route.

Note

Routing is a bare redirect, not a Shopify app install. It works with any storefront (Shopify, a custom cart, a claim page) because it just hands the browser an owner-configured URL. Deeper Shopify-app integration is out of scope here.

Route the validated tap to a VIP claim portal#

The interesting pattern is not "tap goes to product page" — it is tap gates something. Because /t verifies before it redirects, arriving at your claim portal is itself the entitlement check: only a genuine, counter-advanced tap can put a browser there.

A claim flow looks like this:

  1. Set the system route to your claim portal (https://shop.example.com/claim) instead of the product page.
  2. Everyone who taps a genuine tag lands on the portal. Everyone who taps a forgery, replays a captured URL, or hits an unknown/revoked tag sees the "Not verified" tap page and never reaches the portal at all.
  3. The portal collects the email / issues the discount / registers the warranty, and hands off to Shopify checkout as normal.

The security property you are relying on is narrow but strong: the redirect target is owner-stored config, never taken from the tag or the URL. A forgery cannot be steered anywhere — not to your portal, and not to an attacker's.

Warning

The redirect proves a genuine tag was tapped. It does not by itself prove which tag, and the claim-portal URL, once a genuine visitor is on it, is an ordinary URL they can share. If your claim must be one-per-item, bind it to the tap — see the next section.

Pass tap context to the target#

EdgeNFC does not append uid or ctr to your redirect. The 302 sends the browser to the route_url you stored, byte for byte. That is deliberate — the target is owner config, so nothing tag-supplied can be smuggled into it. There are three honest ways to get tap context downstream:

  • Per-tag routes (Enterprise). Store a different route_url per tag with PATCH /api/tags/{uid}https://shop.example.com/claim/item-4821. The tag→URL mapping is server-side config, so the target does know which item it is, and the identifier was never guessable input.
  • Verify it yourself. Point the tag's SUN URL at your own handler and call GET /verify?sys=&uid=&ctr=&mac= from your server. You get the full verdict — { authentic, uid, read_ctr, replay, reason } — and can key a discount code, a claim row, or a CRM record to uid + read_ctr. See Custom web app.
  • Static context in the stored URL. Campaign and UTM parameters you control can simply be part of the route_url you store.

One-time use, enforced by the counter#

The monotonic read_ctr only ever increases, and the server rejects any tap whose counter does not advance (reason: non_monotonic). So the tap itself is single-use: a captured URL replayed later fails verification and never redirects.

Note what this does and does not give you. It makes the tap one-time, for free, with no server-side nonce of your own. To make the reward one-time, record (uid, read_ctr) from a server-side GET /verify and refuse a second claim for the same pair — a guessable claim link is still a guessable claim link, and the counter is what stops the guessing being useful.

Per-tag overrides (Enterprise)#

Point a single tag somewhere different from the rest of its system with PATCH /api/tags/{uid}. A per-tag route_url wins over the system route on an authentic tap, so you can route a VIP tag, a specific product batch, or a recalled unit independently — without re-writing hardware.

curl -X PATCH https://edgenfc.com/api/tags/04ee11ff2233aa \
  -H "authorization: Bearer $TOKEN" \
  -H "content-type: application/json" \
  -d '{ "route_url": "https://shop.example.com/tag/vip" }'

PATCH /api/tags/{uid} is an Enterprise (mgmt_api) feature — non-Enterprise callers get 402 feature_required. The same endpoint can revoke a tag ({ "status": "revoked" }), after which it verifies as not authentic and stops redirecting immediately. Every change is audited in tag_events.

Note

Outbound scan webhooks to your store (fire a Shopify/CRM event on each tap) are a roadmap item, not yet built — see the Custom web app playbook. Today, drive experiences from the redirect plus a server-side GET /verify.

Verify it worked#

  • Tap a genuine tag and confirm the browser lands on your Shopify product page (a 302 to your route_url).
  • Tap the same URL twice: the second tap fails with non_monotonic and does not redirect — proof the counter stops replays.
  • Hand-edit the mac (a forgery) and confirm it shows "Not verified" and never reaches your store.

Next steps#