Custom web app#
For teams building their own registration flow or experience on top of EdgeNFC: verify taps server-side, read analytics, and manage the tag registry — all over plain HTTPS/JSON. No SDK or webhook is required (and, honestly, neither exists yet — see below).
Verify a tap server-side (GET /verify)#
Point your tags' SUN URLs at your own handler, or read the tap params and call GET /verify yourself. It is public, unauthenticated, and rate-limited.
curl "https://edgenfc.com/verify?sys=sys_01H…&uid=04a1b2c3d4e580&ctr=000042&mac=…"A genuine tap returns 200:
{ "authentic": true, "uid": "04a1b2c3d4e580", "read_ctr": 42,
"replay": "ok", "first_seen": false, "verified_at": "2026-08-01T12:00:00Z" }A forged, malformed, or replayed tap also returns 200, with authentic:false and a reason:
{ "authentic": false, "reason": "mac_mismatch" }reason is one of mac_mismatch | malformed | non_monotonic | unknown_uid.
Important
A forgery is a valid request with a false result:
200+authentic:false, never a4xx. Branch on the boolean, not the HTTP status. Useread_ctr(monotonic) andreplayto build one-time claims: a replayed URL comes backnon_monotonic.
Encrypted-PICC mode (picc_data + cmac)#
GET /verify takes the tap parameters in either of the two URL modes the tag can be configured for — pass through whatever the tag emitted:
| Mode | Query parameters | UID on the wire |
|---|---|---|
| Mirror-plain | sys, uid, ctr, mac | mirrored in plaintext |
| Encrypted-PICC (recommended default) | sys, picc_data, cmac | encrypted inside picc_data |
GET /verify?sys=<system>&picc_data=<hex>&cmac=<hex>In encrypted mode the UID and counter travel encrypted; the verifier decrypts them with the tag's diversified meta-read key before checking the MAC and the counter. The response shape is identical — you still get { authentic, uid, read_ctr, replay, reason }, with uid recovered from the decrypted payload. Your application code does not branch on the mode; only the tag's provisioning does.
Do not try to parse picc_data yourself, and do not log it alongside anything that would let you correlate it back to a person — hiding the UID is the entire point of the mode.
Rate limits and errors#
/verify and the /t tap page are public and rate-limited per client and system with a fixed window. Two things to know:
- Being over budget is reported the same way a forgery is:
200withauthentic:falseandreason: "rate_limited"on/verify(and the tap page for/t). Again — branch on the boolean. - Budgets are deployment configuration, not part of the contract. Do not build a client that hammers
/verifyin a loop; verify once per tap, cache your own result, and key it to(uid, read_ctr).
The verify path never returns secret material and compares MACs in constant time, whatever the verdict.
Read analytics (GET /api/analytics, Brand+)#
Aggregate scan data for one system — total scans, the authentic/failed split, a by-country breakdown, and a per-day series. Owner-scoped (404 on a system you don't own) and gated on the Brand plan or higher (402 feature_required).
curl "https://edgenfc.com/api/analytics?sys=sys_01H…" \
-H "authorization: Bearer $TOKEN"{ "system_id": "sys_01H…", "window_days": 30, "total": 128, "authentic": 120, "failed": 8,
"by_country": [ { "country": "US", "count": 90 }, { "country": "GB", "count": 30 } ],
"by_day": [ { "day": "2026-07-15", "count": 12 } ] }The response is privacy-preserving by construction: only counts, ISO-2 country codes, and dates — never a UID, IP, or personal field.
Management auth#
Everything under /api/… is authenticated and owner-scoped, in contrast to the public verify surface:
- Auth: a bearer session token —
authorization: Bearer $TOKEN. There is no separate API key today, and the System Master Key is never an API credential: it derives tag keys, it does not authenticate calls. Never put it in a header, a query string, or a client bundle. - Owner scoping: a system, tag or org belonging to another account returns
404, not403— the resource's existence is hidden. Do not treat404as "deleted". - Feature gates: entitlement is checked at config time, never on the hot tap path. Below the required plan you get
402 feature_required(or402 license_requiredfor the perpetual Edge license). Handle these as "upgrade needed", not as a bug. - Roles: inside an org (Enterprise), a member without the
owner/adminrole on a management action gets403 forbidden. - Provisioning sync: the provisioning app authenticates to
POST /api/tags/importwith a scoped provisioning token — again, not the master key.
Manage tags (management API)#
Drive your registry from your app with the authenticated management routes — all owner-scoped (a resource in another account returns 404):
GET /api/tags?sys=…— list/search the registry.POST /api/tags/import— bulk import a provisioning-app registry export (idempotent byuid).GET /api/tags/{uid}— tag detail (key version, last counter, last seen).PATCH /api/tags/{uid}— re-point (route_url),revoke, or re-activate a tag without re-writing hardware (Enterprisemgmt_api;402 feature_requiredotherwise).
Management errors use one envelope — { "error": { "code", "message" } } — with code in unauthorized | not_found | rate_limited | validation | feature_required | license_required | quota_exceeded. See the REST API reference for the full contract.
SDK — planned (not yet available)#
There is no published SDK yet. For in-app or edge verification today, embed the Wasm core directly (the same Rust core the hosted gateway and the Android app run), as shown in the DIY Edge Core guide — TypeScript types ship with the WASM package. A packaged JS/Wasm SDK with a stable surface is on the roadmap; until then, integrate over REST or against the Wasm module directly.
Webhooks — roadmap (not yet built)#
Note
Outbound scan webhooks are a roadmap item, not a shipping feature. The design — subscribe to tap events with signed, retried, idempotent delivery to drive dynamic product registration or CRM updates — is specified (
spec/17, the G4 milestone) but not yet implemented. Do not build against a webhook endpoint today; none exists.Until webhooks land, drive on-tap behavior with the
GET /verifycall above (poll or verify at your own handler) plus custom routing for redirects. This page will document the live webhook contract once G4 ships.
Verify it worked#
- Call
GET /verifywith a genuine tap →authentic:trueand an advancedread_ctr. - Replay the same params →
authentic:false,reason: non_monotonic. - Call
GET /api/analytics?sys=…and confirm the authentic/failed counts move as you test.
Next steps#
- REST API reference — every endpoint, shape, and error code.
- E-commerce / Shopify — route authentic taps to a destination.
- DIY Edge Core — embed the Wasm core and keep key custody.
EdgeNFC