Developer

Evertag REST API

A thin, authenticated shell over the same repos the dashboard uses. Every request needs a bearer API key; every response — one deliberate exception — is the same { success, data, error } envelope. Webhooks (scan-threshold notifications) are not available yet.

Base URL: https://evertag.app/api/v1

01

Authentication

Every /api/v1 request carries an Authorization: Bearer <secret> header. Keys are minted from your organization's API keys page (dashboard-only — there is no key-creation endpoint).

Authorization: Bearer evtg_8fH2kQmz9pR...
  • Keys start with the evtg_ prefix and are 45+ characters of random, high-entropy text.
  • The full secret is shown exactly once, at creation time. Evertag stores only a SHA-256 hash and a short display prefix — if you lose it, revoke it and create a new one.
  • Each key carries one of two scopes: read (list/get/render/stats) or read,write (also create/update/delete). Write endpoints reject a read-only key with 403 insufficient_scope.
  • Keys are revocable at any time from the dashboard; a revoked key is rejected immediately.

02

Quotas & rate limits

The only guaranteed ceiling is the monthly quota below — it is enforced atomically in Postgres and cannot be exceeded. Everything else on this page under "burst limits" is advisory, best-effort protection against runaway clients, not a contract: it lives in a KV counter that is deliberately swallowed on any infrastructure hiccup, so it can under-enforce but will never wrongly block a request that should succeed.

API calls per month by plan, read from the TIERS constant
PlanPriceDynamic codesSeatsAPI calls / month
FreeFree51100
Starter$9/mo5031,000
Growth$19/mo2501010,000
Business$39/mo1,0002550,000

Once the monthly quota is reached, every call returns 429 quota_exceeded with a retry-after header (seconds until the UTC month rolls over) until it resets.

Burst limits (advisory)

  • Per organization: ~60 calls/minute. The 61st call inside a wall-clock minute returns 429 rate_limited.
  • Per IP, pre-authentication: ~120 calls/minute, checked before your key is even verified — this protects against a flood of garbage keys, not against a legitimate integration.
  • If the rate-limit store is unavailable, burst checks are skipped entirely rather than blocking traffic — only the monthly quota above is ever a hard stop.

03

Response envelope

Every JSON response — success or failure — shares one shape. The single exception is qr.svg, which returns a raw image on success (see below).

// success
{
  "success": true,
  "data": {
        …
        },
  "error": null
}

// failure
{
  "success": false,
  "data": null,
  "error": {
    "code": "invalid_request",
    "message": "human-readable detail"
  }
}

04

Endpoints

File codes (type: 'file') require a multipart upload and can only be created from the dashboard — POST /codes rejects them with 400 invalid_request.

GET /api/v1/codes

List every non-archived code owned by the key's organization. Read scope suffices.

curl https://evertag.app/api/v1/codes \
  -H "Authorization: Bearer evtg_..."
{
  "success": true,
  "data": [
    {
      "id": "b7e6b6b0-2a2f-4a2b-9c3d-8f0a2c1e9a11",
      "name": "Table tent",
      "slug": "8f3k2a",
      "type": "url",
      "state": "active",
      "destination": "https://example.com/menu",
      "config": { "destination": "https://example.com/menu" },
      "shortUrl": "https://evertag.app/s/8f3k2a",
      "createdAt": "2026-07-01T12:00:00.000Z",
      "updatedAt": "2026-07-01T12:00:00.000Z"
    }
  ],
  "error": null
}
POST /api/v1/codes

Write scope required. Creates a code and counts against the plan's dynamic-code limit.

FieldTypeNotes
namestringrequired, non-empty
typestringone of url, vcard, wifi, linkpage, menu, appstore (not file) — see Code types below
configobjectshape depends on type, validated server-side
curl -X POST https://evertag.app/api/v1/codes \
  -H "Authorization: Bearer evtg_..." \
  -H "Content-Type: application/json" \
  -d '{
    "type": "url",
    "name": "Table tent",
    "config": { "destination": "https://example.com/menu" }
  }'
// 201 Created
{
  "success": true,
  "data": {
    "id": "b7e6b6b0-2a2f-4a2b-9c3d-8f0a2c1e9a11",
    "name": "Table tent",
    "slug": "8f3k2a",
    "type": "url",
    "state": "active",
    "destination": "https://example.com/menu",
    "config": { "destination": "https://example.com/menu" },
    "shortUrl": "https://evertag.app/s/8f3k2a",
    "createdAt": "2026-07-01T12:00:00.000Z",
    "updatedAt": "2026-07-01T12:00:00.000Z"
  },
  "error": null
}
GET /api/v1/codes/:id

Read a single code. Read scope suffices. A foreign or unknown id returns 404 not_found.

curl https://evertag.app/api/v1/codes/b7e6b6b0-2a2f-4a2b-9c3d-8f0a2c1e9a11 \
  -H "Authorization: Bearer evtg_..."
{
  "success": true,
  "data": {
    "id": "b7e6b6b0-2a2f-4a2b-9c3d-8f0a2c1e9a11",
    "name": "Table tent",
    "slug": "8f3k2a",
    "type": "url",
    "state": "active",
    "destination": "https://example.com/menu",
    "config": { "destination": "https://example.com/menu" },
    "shortUrl": "https://evertag.app/s/8f3k2a",
    "createdAt": "2026-07-01T12:00:00.000Z",
    "updatedAt": "2026-07-01T12:00:00.000Z"
  },
  "error": null
}
PATCH /api/v1/codes/:id

Write scope required. Send exactly one of config or state.

FieldTypeNotes
configobjectrevalidated against the code's (immutable) type
statestring'active' or 'paused' only — 'redirect_only' is a system-managed state and is rejected with 400 invalid_request if requested explicitly
curl -X PATCH https://evertag.app/api/v1/codes/b7e6b6b0-2a2f-4a2b-9c3d-8f0a2c1e9a11 \
  -H "Authorization: Bearer evtg_..." \
  -H "Content-Type: application/json" \
  -d '{ "state": "paused" }'
{
  "success": true,
  "data": {
    "id": "b7e6b6b0-2a2f-4a2b-9c3d-8f0a2c1e9a11",
    "name": "Table tent",
    "slug": "8f3k2a",
    "type": "url",
    "state": "paused",
    "destination": "https://example.com/menu",
    "config": { "destination": "https://example.com/menu" },
    "shortUrl": "https://evertag.app/s/8f3k2a",
    "createdAt": "2026-07-01T12:00:00.000Z",
    "updatedAt": "2026-07-01T12:05:00.000Z"
  },
  "error": null
}
DELETE /api/v1/codes/:id

Write scope required. Archives the code — a terminal state. Archiving removes it from the edge KV lookup, so the short URL stops resolving; there is no API to reverse it.

curl -X DELETE https://evertag.app/api/v1/codes/b7e6b6b0-2a2f-4a2b-9c3d-8f0a2c1e9a11 \
  -H "Authorization: Bearer evtg_..."
{
  "success": true,
  "data": {
    "id": "b7e6b6b0-2a2f-4a2b-9c3d-8f0a2c1e9a11",
    "name": "Table tent",
    "slug": "8f3k2a",
    "type": "url",
    "state": "archived",
    "destination": "https://example.com/menu",
    "config": { "destination": "https://example.com/menu" },
    "shortUrl": "https://evertag.app/s/8f3k2a",
    "createdAt": "2026-07-01T12:00:00.000Z",
    "updatedAt": "2026-07-01T12:10:00.000Z"
  },
  "error": null
}
GET /api/v1/codes/:id/qr.svg

Render the code's QR as SVG. Read scope suffices. Optional ?size= in pixels, clamped to 64–2048 (default 256).

The render reflects whatever design — colors, module/eye shape, center logo — is currently saved for the code in the dashboard's Design panel; codes with no saved design render with the classic black-on-white defaults. Mutating a code's design via the API is on the roadmap and not yet supported — for now, set it from the dashboard.

Not <img>-embeddable. This endpoint requires the same Authorization bearer header as every other call — browsers do not attach custom headers to an <img src> request, so hotlinking this URL directly in HTML will fail with 401 unauthorized. Fetch it server-side (or with curl) and serve or embed the bytes yourself.

curl -o qr.svg "https://evertag.app/api/v1/codes/b7e6b6b0-2a2f-4a2b-9c3d-8f0a2c1e9a11/qr.svg?size=512" \
  -H "Authorization: Bearer evtg_..."

Exception to the envelope: on success this returns the raw SVG body with content-type: image/svg+xml — not the JSON envelope. Errors (e.g. unknown or foreign id) still return the standard 404 not_found JSON envelope.

<!-- 200 OK, content-type: image/svg+xml, cache-control: private, max-age=300 -->
<svg width="512" height="512" viewBox="0 0 512 512" ...>...</svg>
GET /api/v1/codes/:id/stats

Lifetime scan totals plus a daily series. Read scope suffices. Optional ?days=, clamped to 1–90 (default 14).

curl "https://evertag.app/api/v1/codes/b7e6b6b0-2a2f-4a2b-9c3d-8f0a2c1e9a11/stats?days=7" \
  -H "Authorization: Bearer evtg_..."
{
  "success": true,
  "data": {
    "scans": 142,
    "uniques": 98,
    "series": [
      { "date": "2026-06-25", "scans": 12, "uniques": 9 },
      { "date": "2026-06-26", "scans": 20, "uniques": 15 }
    ]
  },
  "error": null
}

05

Error codes

HTTP statuserror.codeMeaning
401unauthorizedMissing, malformed, unknown, or revoked API key.
403insufficient_scopeA read-only key attempted a write endpoint.
429rate_limitedAdvisory per-minute burst limit hit (org or pre-auth IP). Retry after the retry-after header.
429quota_exceededMonthly API call quota for the plan reached — the one hard ceiling. Retry after retry-after.
402code_quota_exceededThe org's dynamic-code limit for its plan is full; create fails until a code is archived or the plan is upgraded.
400invalid_requestMalformed JSON, missing/invalid fields, an unknown code type, a file create attempt, or an illegal state value.
404not_foundThe code doesn't exist, or belongs to another organization.
500internal_errorUnhandled server error. The real cause is logged server-side and never echoed to the caller.

06

Code types

Six of the seven code types can be created through the API. file codes need a multipart R2 upload and are dashboard-only.

TypeConfig fields
urldestination required — an http(s) URL
vcardfullName required; optional title, organization, phone, email, url
wifissid required; security required — one of WPA, WEP, nopass; password (required for WPA/WEP, 8–63 chars; omitted for nopass)
linkpagetitle required; links required — array of 1–20 { title, url } entries
menurestaurant required; sections required — array of 1–10 { name, items }, each with 1–50 { name, price } items
appstorefallback required — http(s) URL; at least one of ios or android (both optional http(s) URLs)
fileDashboard-only — not creatable via POST /codes.

redirect_only is a seventh, system-managed state (not a type) — Evertag flips a code into it automatically in specific fallback scenarios; it can never be set through the API.