API documentation

Everything in the dashboard is available over a REST API and a native MCP server, metered in the same prepaid credits. Base URL: https://taproute.io

Authentication

Create API keys in Dashboard → API. Every request needs a bearer header. Keys start with tr_ and are shown exactly once at creation.

Authorization: Bearer tr_...
curl https://taproute.io/api/v1/links \
  -H "Authorization: Bearer tr_your_key_here"

Endpoints

MethodPathBodyReturns
POST/api/v1/links{url, slug?, domain?, rules?, design?}201 · {id, slug, domain, shortUrl}
GET/api/v1/linksNone{links: [...]}
PATCH/api/v1/links/:id{url?, status?, rules?, design?, page?}{id, slug, url, status}
DELETE/api/v1/links/:idNone{ok: true, pageTitles: [...]}
GET/api/v1/walletNone{balance, inGrace, …}
GET/api/v1/analytics?days=7|30|90None{total, timeseries, byCountry, byDevice, topLinks}

Links

curl -X POST https://taproute.io/api/v1/links \
  -H "Authorization: Bearer tr_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/menu",
    "slug": "summer-menu",
    "rules": { "ios": "https://apps.apple.com/app/id123" }
  }'

# 201 Created
# { "id": "…", "slug": "summer-menu", "domain": "_", "shortUrl": "https://tap2u.link/summer-menu" }
# Change the destination after printing: the QR keeps working
curl -X PATCH https://taproute.io/api/v1/links/LINK_ID \
  -H "Authorization: Bearer tr_..." \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/winter-menu", "status": "active" }'

Custom domains

Pass domain to put a link on a hostname you have connected in Dashboard → Domains. Omit it and the link lives on the shared short domain (https://tap2u.link). The shortUrl we return is the one to encode. A QR built against the wrong host is printed and unfixable.

# Put the link on one of your own verified domains.
# Slugs are unique PER DOMAIN, so "summer-menu" can exist on both.
curl -X POST https://taproute.io/api/v1/links \
  -H "Authorization: Bearer tr_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/menu",
    "slug": "summer-menu",
    "domain": "qr.yourbrand.com"
  }'

# 201 Created
# { "id": "…", "slug": "summer-menu", "domain": "qr.yourbrand.com",
#   "shortUrl": "https://qr.yourbrand.com/summer-menu" }

Wallet

curl https://taproute.io/api/v1/wallet -H "Authorization: Bearer tr_..."

# { "balance": 48210, "inGrace": false, "graceExhausted": false, … }

Analytics

curl "https://taproute.io/api/v1/analytics?days=30" -H "Authorization: Bearer tr_..."

# { "total": 1289, "timeseries": [{ "date": "2026-07-01", "count": 42 }, …],
#   "byCountry": […], "byDevice": […], "topLinks": […] }

The rules object

Optional per-link routing, evaluated at the edge on every scan. All fields are optional; anything unmatched falls through to the link's main url.

{
  "ios": "https://apps.apple.com/app/id123456",
  "android": "https://play.google.com/store/apps/details?id=com.example",

  // First matching window wins. Absolute (ISO start/end) …
  "schedule": [
    { "start": "2026-07-01T08:00:00Z", "end": "2026-07-01T11:00:00Z", "url": "https://example.com/breakfast" },
    // … or recurring: days 0=Sun..6=Sat, 'HH:MM' in "tz" (default: the scanner's timezone)
    { "days": [1, 2, 3, 4, 5], "from": "09:00", "to": "17:00", "tz": "Europe/Berlin", "url": "https://example.com/open" }
  ],

  // Country code (from the edge) → destination
  "geo": { "DE": "https://example.com/de", "FR": "https://example.com/fr" },
  // Accept-Language prefix → destination
  "lang": { "he": "https://example.com/he" },

  // Hard scan cap — past it, the paused page is shown
  "cap": 10000,
  // SHA-256 hex of an access password; the edge shows a gate form
  "passwordHash": "9f86d081…",
  // Verified-brand preview interstitial (anti-quishing); auto = ms before auto-continue
  "preview": { "brand": "Acme Coffee", "domain": "acme.com", "auto": 2000 }
}
  • ios / android · device-specific destinations (App Store / Play Store deep links).
  • schedule · array of time windows with ISO 8601 datetimes. start and end are each optional (open-ended windows). The first matching window wins.

Webhooks

Add endpoints in Dashboard → API to receive a POST for every scan, delivered from the edge in real time. Each request carries two headers:

X-Taproute-EventEvent name, currently scan
X-Taproute-Signaturet=<ms>,v1=<hex hmac-sha256(secret, t + "." + body)>

Payload

{
  "event": "scan",
  "data": {
    "linkId": "9f1c…",
    "slug": "summer-menu",
    "domain": "_",
    "country": "DE",
    "device": "ios",
    "ts": 1751884800000
  }
}

Verifying signatures (Node)

import { createHmac, timingSafeEqual } from 'node:crypto';

// signature header: t=<ms>,v1=<hex>
export function verifyTaproute(secret, signature, rawBody) {
  const parts = Object.fromEntries(signature.split(',').map((p) => p.split('=')));
  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');
  return timingSafeEqual(Buffer.from(parts.v1, 'hex'), Buffer.from(expected, 'hex'));
}

MCP server

A native Model Context Protocol server (Streamable HTTP) lets AI agents (Claude, Cursor, and friends) manage your QR codes directly. Authenticate with the same API keys:

{
  "mcpServers": {
    "taproute": {
      "url": "https://taproute.io/mcp",
      "headers": { "Authorization": "Bearer tr_..." }
    }
  }
}
create_linkCreate a dynamic QR link, optionally on one of your custom domains. Returns the short URL to encode
list_linksList all links with status, domain and destinations
update_linkChange destination, rules, or status (active | paused)
delete_linkDelete a link permanently
get_qrSVG QR for one of your links, encoding that link’s own domain. Errors if the slug is not yours. It never returns a QR for a link that does not exist
wallet_balanceCredit balance and grace-period status
analytics_summaryScan analytics for 7 / 30 / 90 days

Errors

Standard HTTP status codes with a JSON body of the shape { "message": "…" }.

400Malformed input: missing url, invalid slug, bad JSON
401Missing or invalid API key
404Link not found (or belongs to another workspace)
409Slug already taken
422Destination rejected by safety screening (malware / phishing lists)
429Link-creation rate limit reached. Retry in an hour