Amba

QR Codes

Account-scoped trackable QR codes — create, edit, stats and framed images.

QR codes belong to the Amba account, so these routes take a developer Bearer (a personal access token or a sign-in session) and no project id. Server keys and project-scoped tokens are refused with 403 QR_DEVELOPER_TOKEN_REQUIRED. See QR Codes for how scans, safety checks and plans work.

Endpoints

MethodPathDescription
POST/qrCreate a code.
GET/qrList your codes, newest first.
GET/qr/:idOne code with its destination history.
PATCH/qr/:idEdit a code or turn it off and on.
DELETE/qr/:idDelete a code.
GET/qr/:id/statsDaily series and, on Pro, breakdowns.
GET/qr/:id/export.csvDaily stats as CSV (Pro).
GET/qr/:id/imageFramed SVG or PNG.
GET/qr/meYour display name and QR plan.
PATCH/qr/meSet the display name scanners see.
GET/qr/billingYour QR plan, subscription status and renewal date.
POST/qr/billing/checkoutA checkout link for QR Pro ($10/month).
POST/qr/billing/portalA billing portal link (card, invoices, cancel).
POST/qr/sharesShare some codes with a brand viewer.
GET/qr/sharesList your shares.
DELETE/qr/shares/:shareIdTurn a share link off.
GET/qr/share-view/:tokenA share's stats (no Bearer; the token is the credential).
POST/auth/creator/startEmail a creator sign-up code (no Bearer).
POST/auth/creator/verifyRedeem it for a session and PAT (no Bearer).

The code object

{
  "id": "6f1c2a9e-…",
  "slug": "k7m2xq9",
  "url": "https://go.amba.host/k7m2xq9",
  "label": "Acme jacket try-on",
  "destination_url": "https://shop.acme.com/jacket",
  "destination_host": "shop.acme.com",
  "final_url": "https://shop.acme.com/jacket?utm_source=amba_qr&utm_medium=tiktok&utm_campaign=k7m2xq9&utm_content=k7m2xq9",
  "platform": "tiktok",
  "post_url": null,
  "campaign": null,
  "sponsored": true,
  "sponsor_name": "Acme",
  "utm_mode": "append",
  "status": "active",
  "disabled_reason": null,
  "disabled_at": null,
  "safety": { "verdict": "safe", "checked_at": "2026-10-19T12:00:00.000Z" },
  "scan_count": 0,
  "continue_count": 0,
  "created_at": "2026-10-19T12:00:00.000Z",
  "updated_at": "2026-10-19T12:00:00.000Z",
  "image_urls": {
    "svg": "https://api.amba.dev/v1/qr/6f1c2a9e-…/image?format=svg&exp=…&sig=…",
    "png": "https://api.amba.dev/v1/qr/6f1c2a9e-…/image?format=png&exp=…&sig=…",
    "expires_at": "2026-10-26T12:00:00.000Z"
  }
}

status is active, disabled_owner (you turned it off) or disabled_safety (Amba turned it off for review). safety.verdict is pending, safe or unsafe. image_urls open without a Bearer until expires_at.

POST /qr

Request

FieldTypeRequiredNotes
destination_urlstringyesAbsolute http(s) URL; must pass the safety checks
labelstringnoUp to 200 characters; defaults to the destination domain
platformstringnotiktok, instagram, youtube, x, snapchat, twitch, facebook, linkedin, other (default)
post_urlstringnohttp(s) link to the post
campaignstringnoUp to 100 characters; becomes utm_campaign
sponsoredbooleannoDefault false
sponsor_namestringwhen sponsoredUp to 100 characters
utm_modestringnoappend (default) or off

Response 201

{ "qr_code": { "id": "…", "slug": "k7m2xq9", "url": "https://go.amba.host/k7m2xq9", "…": "…" } }

Errors

  • 400 INVALID_JSON — the body is not a JSON object.
  • 400 VALIDATION_ERROR — a field is malformed; details.field names it.
  • 400 QR_DESTINATION_REJECTED — the destination breaks a safety rule; details.reason names it.
  • 400 QR_DESTINATION_UNSAFE — the destination is on the list of known malware and phishing sites.
  • 403 QR_ACCOUNT_UNVERIFIED — the account has no proven email address yet; claim it first.
  • 403 QR_ACCOUNT_SUSPENDED — Amba suspended QR codes on the account for review.
  • 429 QR_DAILY_CAP — the account's 24-hour cap on new codes is reached; details.limit gives it.
  • 429 RATE_LIMIT_EXCEEDED — more than 30 creates and destination changes in a minute; see Retry-After.

Try it:

POST/qr
developer auth
curl -X POST 'https://api.amba.dev/v1/qr' \
  -H 'Content-Type: application/json' \
  -d '{
  "destination_url": "https://example.com/product",
  "platform": "tiktok"
}'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST "${BASE_URL}/qr" \
  -H "Authorization: Bearer ${DEV_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{"destination_url":"https://example.com/product","platform":"tiktok"}'

GET /qr

Query: status (active, disabled or all, the default), limit (1 to 200, default 50), offset. Deleted codes are left out.

{
  "qr_codes": [{ "id": "…", "slug": "k7m2xq9", "scan_count": 12, "…": "…" }],
  "total": 1,
  "limit": 50,
  "offset": 0
}

Try it:

GET/qr
developer auth
curl -X GET 'https://api.amba.dev/v1/qr'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X GET "${BASE_URL}/qr?status=active" \
  -H "Authorization: Bearer ${DEV_TOKEN}"

GET /qr/:id

The code object plus destination_history, newest first (the latest 500 changes):

{
  "qr_code": {
    "id": "…",
    "destination_history": [
      {
        "old_url": "https://one.example.com/",
        "new_url": "https://two.example.com/",
        "changed_at": "…",
        "changed_by": "owner"
      },
      {
        "old_url": null,
        "new_url": "https://one.example.com/",
        "changed_at": "…",
        "changed_by": "owner"
      }
    ]
  }
}

404 QR_NOT_FOUND for a code that is not yours or was deleted.

Try it:

GET/qr/%7B%7BqrId%7D%7D
developer auth
curl -X GET 'https://api.amba.dev/v1/qr/%7B%7BqrId%7D%7D'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X GET "${BASE_URL}/qr/${QR_ID}" \
  -H "Authorization: Bearer ${DEV_TOKEN}"

PATCH /qr/:id

Send any of the create fields, plus status (active or disabled_owner). post_url, campaign and sponsor_name accept null to clear. A changed destination_url runs the safety checks again, counts toward the destination-change cap, and is added to the destination history.

Errors

  • 400 VALIDATION_ERROR, 400 QR_DESTINATION_REJECTED, 400 QR_DESTINATION_UNSAFE — as on create.
  • 404 QR_NOT_FOUND.
  • 403 QR_ACCOUNT_SUSPENDED — the account is suspended, and the change would turn the code on or change its destination.
  • 409 QR_DISABLED_FOR_SAFETY — Amba turned the code off for review; it cannot be edited.
  • 429 QR_DAILY_CAP — the 24-hour cap on destination changes is reached.

Try it:

PATCH/qr/%7B%7BqrId%7D%7D
developer auth
curl -X PATCH 'https://api.amba.dev/v1/qr/%7B%7BqrId%7D%7D' \
  -H 'Content-Type: application/json' \
  -d '{
  "status": "disabled_owner"
}'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X PATCH "${BASE_URL}/qr/${QR_ID}" \
  -H "Authorization: Bearer ${DEV_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{"destination_url":"https://example.com/new"}'

DELETE /qr/:id

{ "deleted": true, "id": "…" }

Scanners see the turned-off page from then on. The slug is never given to another code.

Try it:

DELETE/qr/%7B%7BqrId%7D%7D
developer auth
curl -X DELETE 'https://api.amba.dev/v1/qr/%7B%7BqrId%7D%7D'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X DELETE "${BASE_URL}/qr/${QR_ID}" \
  -H "Authorization: Bearer ${DEV_TOKEN}"

GET /qr/:id/stats

Query: from, to as YYYY-MM-DD (UTC). Defaults to the last 30 days; at most 400 days per request.

{
  "qr_id": "…",
  "slug": "k7m2xq9",
  "plan": "pro",
  "from": "2026-09-20",
  "to": "2026-10-19",
  "history_limited": false,
  "totals": { "scans": 120, "continues": 0, "visitor_days": 97 },
  "daily": [{ "day": "2026-09-20", "scans": 4, "continues": 0, "unique_visitors": 3 }],
  "by_country": [{ "country": "US", "scans": 80, "continues": 0, "unique_visitors": 64 }],
  "by_device_os": [{ "device_os": "ios", "scans": 70, "continues": 0, "unique_visitors": 55 }]
}

On the free plan, from moves up to 30 days ago, history_limited is true, and by_country / by_device_os are null. country is null when unknown. Bots and link previews are left out of every number. Visitor ids rotate daily, so totals.visitor_days adds up each day's unique_visitors.

Try it:

GET/qr/%7B%7BqrId%7D%7D/stats
developer auth
curl -X GET 'https://api.amba.dev/v1/qr/%7B%7BqrId%7D%7D/stats'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X GET "${BASE_URL}/qr/${QR_ID}/stats?from=2026-10-01&to=2026-10-19" \
  -H "Authorization: Bearer ${DEV_TOKEN}"

GET /qr/:id/image

Query: format (svg, the default, or png), size (pixels wide, 256 to 2048; PNG default 1024), and on Pro fg / bg hex colours. Without a Bearer, the request needs the exp and sig of a signed link from image_urls.

Errors

  • 400 VALIDATION_ERROR — bad format, size or colour.
  • 400 QR_COLORS_LOW_CONTRAST — fg must be darker than bg with a contrast ratio of at least 4:1.
  • 402 QR_PRO_REQUIRED — custom colours on the free plan. details.upgrade names the upgrade call (POST /qr/billing/checkout, MCP amba_qr_upgrade).
  • 429 RATE_LIMIT_EXCEEDED — more than 20 fresh PNG renders in a minute from one caller.
  • 404 QR_NOT_FOUND.

Try it:

GET/qr/%7B%7BqrId%7D%7D/image
developer auth
curl -X GET 'https://api.amba.dev/v1/qr/%7B%7BqrId%7D%7D/image'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -o qr.png "${BASE_URL}/qr/${QR_ID}/image?format=png&size=1024" \
  -H "Authorization: Bearer ${DEV_TOKEN}"

GET /qr/:id/export.csv

Pro. The code's daily stats as CSV: one row per UTC day, country and device OS, the finest detail Amba keeps. Query: from, to as YYYY-MM-DD (UTC), both optional; without them the export covers the code's whole history. Bots and link previews are left out of every count, and country is empty when unknown.

day,country,device_os,scans,continues,unique_visitors
2026-09-01,,android,1,0,1
2026-09-01,FR,ios,3,1,3
2026-09-02,US,ios,5,2,4

Errors

  • 402 QR_PRO_REQUIRED — CSV export on the free plan. details.upgrade names the upgrade call.
  • 400 VALIDATION_ERROR — a date that is not YYYY-MM-DD, or from after to.
  • 400 QR_EXPORT_TOO_LARGE — the range holds more than 200,000 rows; export a shorter one with from and to.
  • 429 RATE_LIMIT_EXCEEDED — more than 10 exports in a minute from one account (Retry-After says when to retry).
  • 404 QR_NOT_FOUND.

Try it:

GET/qr/%7B%7BqrId%7D%7D/export.csv
developer auth
curl -X GET 'https://api.amba.dev/v1/qr/%7B%7BqrId%7D%7D/export.csv'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -o qr-stats.csv "${BASE_URL}/qr/${QR_ID}/export.csv?from=2026-09-01" \
  -H "Authorization: Bearer ${DEV_TOKEN}"

GET /qr/me

{
  "me": {
    "id": "…",
    "email": "sam@example.com",
    "display_name": "Sam Rivera",
    "shown_as": "Sam Rivera",
    "account_kind": "creator",
    "qr_plan": "free"
  }
}

shown_as is what scanners see on the disclosure page ("An Amba creator" without a display name).

Try it:

GET/qr/me
developer auth
curl -X GET 'https://api.amba.dev/v1/qr/me'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X GET "${BASE_URL}/qr/me" -H "Authorization: Bearer ${DEV_TOKEN}"

PATCH /qr/me

Body: { "display_name": "Sam Rivera" } (1 to 80 characters with no links, domains or "Amba"; null clears it). 400 VALIDATION_ERROR for a name that breaks those rules; 429 RATE_LIMIT_EXCEEDED past 10 changes an hour.

Try it:

PATCH/qr/me
developer auth
curl -X PATCH 'https://api.amba.dev/v1/qr/me' \
  -H 'Content-Type: application/json' \
  -d '{
  "display_name": "Sam Rivera"
}'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X PATCH "${BASE_URL}/qr/me" \
  -H "Authorization: Bearer ${DEV_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{"display_name":"Sam Rivera"}'

GET /qr/billing

Your account's QR plan. QR Pro is $10/month per account, billed apart from any project plan.

{
  "billing": {
    "plan": "pro",
    "status": "active",
    "renews_at": "2026-11-20T00:00:00.000Z",
    "ends_at": null,
    "comped": false,
    "price": { "amount_usd": 10, "interval": "month" },
    "upgrade_available": false
  }
}
  • status is the QR Pro subscription's status (active, trialing, past_due, canceled, unpaid, incomplete, incomplete_expired, paused), or none before the first one.
  • renews_at is the next renewal while the subscription renews. ends_at is set when a canceled subscription runs to the end of its paid period; the plan stays Pro until then.
  • comped is true when Amba gave the account Pro at no charge.
  • upgrade_available is true when POST /qr/billing/checkout would start a checkout.

The plan follows the subscription: Pro while it is active or trialing (and for 14 days while a failed renewal is retried), Free once it ends.

Try it:

GET/qr/billing
developer auth
curl -X GET 'https://api.amba.dev/v1/qr/billing'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X GET "${BASE_URL}/qr/billing" -H "Authorization: Bearer ${DEV_TOKEN}"

POST /qr/billing/checkout

No body. Returns a checkout link for QR Pro ($10/month) that the account owner opens to pay. Pro applies as soon as checkout completes, and the dashboard on go.amba.host/me is where checkout returns.

{
  "url": "https://…/c/pay/cs_…",
  "session_id": "cs_…",
  "expires_at": "2026-10-21T12:00:00.000Z",
  "price": { "amount_usd": 10, "interval": "month" }
}

Errors

  • 409 QR_PRO_ACTIVE — the account already has QR Pro: a subscription (use POST /qr/billing/portal to manage it) or Pro at no charge from Amba.
  • 409 QR_PRO_PAYMENT_PENDING — the first payment of the account's QR Pro subscription is still being confirmed; it completes or expires within a day.
  • 409 QR_CHECKOUT_IN_PROGRESS — another checkout for the account is running; retry in a moment.
  • 403 QR_ACCOUNT_SUSPENDED, 403 QR_ACCOUNT_UNVERIFIED — the same account checks as creating a code.
  • 503 QR_PRO_NOT_AVAILABLE — QR Pro is not available in this environment yet.
  • 502 QR_BILLING_UNAVAILABLE — the payment provider did not answer; retry.

Try it:

POST/qr/billing/checkout
developer auth
curl -X POST 'https://api.amba.dev/v1/qr/billing/checkout' \
  -H 'Content-Type: application/json' \
  -d '{}'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST "${BASE_URL}/qr/billing/checkout" -H "Authorization: Bearer ${DEV_TOKEN}"

POST /qr/billing/portal

No body. Returns { "url": "…" }, a billing portal link where the account owner changes the card, reads invoices or cancels QR Pro. The portal covers QR Pro alone; project plans have their own (POST /admin/projects/:projectId/billing/portal).

Errors

  • 409 QR_NO_SUBSCRIPTION — the account has no QR Pro billing yet; upgrade first.
  • 502 QR_BILLING_UNAVAILABLE — the payment provider did not answer; retry.

Try it:

POST/qr/billing/portal
developer auth
curl -X POST 'https://api.amba.dev/v1/qr/billing/portal' \
  -H 'Content-Type: application/json' \
  -d '{}'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST "${BASE_URL}/qr/billing/portal" -H "Authorization: Bearer ${DEV_TOKEN}"

POST /qr/shares

Request

FieldTypeRequiredNotes
namestringyesUp to 200 characters, e.g. the brand
viewer_emailstringyesAmba emails the share link here
qr_idsstring[]yes1 to 100 of your code ids

Response 201

{
  "share": {
    "id": "…",
    "name": "Acme",
    "viewer_email": "brand@acme.com",
    "qr_ids": ["…"],
    "status": "active",
    "url": "https://go.amba.host/s/…",
    "email_sent": true,
    "created_at": "…",
    "last_viewed_at": null,
    "revoked_at": null
  }
}

url is returned only here. Anyone with it can view the share, so it goes to the viewer and to you.

Errors

  • 400 VALIDATION_ERROR — a field is malformed, or a name carries a link, a domain or "Amba".
  • 403 QR_SHARE_LIMIT — the free plan keeps one active share. details.upgrade names the upgrade call.
  • 403 QR_ACCOUNT_SUSPENDED — Amba suspended QR codes on the account.
  • 404 QR_NOT_FOUND — a code id is not yours, or was deleted.
  • 409 QR_DISABLED_FOR_SAFETY — a code Amba turned off for safety cannot be shared.
  • 429 QR_DAILY_CAP — 20 shares per 24 hours, or 3 for the same viewer.
  • 503 QR_SHARES_NOT_AVAILABLE — brand shares open together with the brand dashboard page.

email_sent is false when the invitation could not go out (a daily email budget, a delivery failure); the share still works through url.

Try it:

POST/qr/shares
developer auth
curl -X POST 'https://api.amba.dev/v1/qr/shares' \
  -H 'Content-Type: application/json' \
  -d '{
  "name": "Acme",
  "viewer_email": "brand@example.com",
  "qr_ids": [
    "{{qrId}}"
  ]
}'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X POST "${BASE_URL}/qr/shares" \
  -H "Authorization: Bearer ${DEV_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Acme","viewer_email":"brand@example.com","qr_ids":["'"${QR_ID}"'"]}'

GET /qr/shares

{
  "shares": [
    {
      "id": "…",
      "name": "Acme",
      "viewer_email": "brand@acme.com",
      "qr_ids": ["…"],
      "status": "active",
      "last_viewed_at": "…"
    }
  ]
}

Try it:

GET/qr/shares
developer auth
curl -X GET 'https://api.amba.dev/v1/qr/shares'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X GET "${BASE_URL}/qr/shares" -H "Authorization: Bearer ${DEV_TOKEN}"

DELETE /qr/shares/:shareId

{ "revoked": true, "id": "…" }

Try it:

DELETE/qr/shares/%7B%7BshareId%7D%7D
developer auth
curl -X DELETE 'https://api.amba.dev/v1/qr/shares/%7B%7BshareId%7D%7D'
Loading auth… Configure auth in the settings drawer (top-right) to run this request.

Curl:

curl -X DELETE "${BASE_URL}/qr/shares/${SHARE_ID}" -H "Authorization: Bearer ${DEV_TOKEN}"

GET /qr/share-view/:token

No Bearer: the token from the share link is the credential. Query: from, to as on stats, following the sharer's plan.

{
  "share": { "name": "Acme", "creator_display_name": "Sam Rivera", "created_at": "…" },
  "plan": "free",
  "from": "2026-09-20",
  "to": "2026-10-19",
  "history_limited": true,
  "codes": [
    {
      "slug": "k7m2xq9",
      "url": "https://go.amba.host/k7m2xq9",
      "label": "Acme jacket try-on",
      "destination_url": "https://shop.acme.com/jacket",
      "final_url": "https://shop.acme.com/jacket?utm_source=amba_qr&…",
      "status": "active",
      "totals": { "scans": 120, "continues": 64, "visitor_days": 97 },
      "daily": [{ "day": "2026-09-20", "scans": 4, "continues": 2, "unique_visitors": 3 }],
      "by_country": null,
      "by_device_os": null,
      "destination_history": [
        {
          "old_url": null,
          "new_url": "https://shop.acme.com/jacket",
          "changed_at": "…",
          "changed_by": "owner"
        }
      ]
    }
  ]
}

A code Amba turned off for safety appears with destination_url, destination_host and final_url set to null and an empty destination_history. 404 QR_SHARE_NOT_FOUND for an unknown or turned-off share, or one whose owner Amba suspended; 429 RATE_LIMIT_EXCEEDED past 60 views a minute from one caller or 30 a minute of one share.

Curl:

curl -X GET "${BASE_URL}/qr/share-view/${SHARE_TOKEN}"

POST /auth/creator/start

No Bearer. Body: { "email": "sam@example.com" }. Emails a 6-digit code and answers { "data": { "ok": true } }.

  • 400 DISPOSABLE_EMAIL — disposable addresses are refused.
  • 403 CREATOR_SIGNUPS_CLOSED — creator signups are closed right now.
  • 429 CREATOR_SIGNUPS_DAILY_CAP — the daily limit on new creator accounts is reached.
  • 429 RATE_LIMIT_EXCEEDED or 429 EMAIL_BUDGET_EXCEEDED — too many codes for this address or network.

Curl:

curl -X POST "${BASE_URL}/auth/creator/start" \
  -H 'Content-Type: application/json' -d '{"email":"sam@example.com"}'

POST /auth/creator/verify

No Bearer. Body: { "email", "code", "display_name"? }.

{
  "data": {
    "access_token": "…",
    "refresh_token": "…",
    "pat": "amb_dpat_…",
    "created": true,
    "developer": {
      "id": "…",
      "email": "sam@example.com",
      "display_name": "Sam Rivera",
      "account_kind": "creator",
      "qr_plan": "free"
    }
  }
}

pat is set only when the call creates the account. An address that already has an account signs in with pat: null and keeps its token.

  • 400 INVALID_INPUT — a malformed field, or a display_name with a link, a domain or "Amba".
  • 401 CREATOR_CODE_INVALID — the code is wrong, used, locked or expired.
  • 409 CREATOR_ACCOUNT_UNPROVEN — an Amba account that never proved this address uses it; sign in with a code from POST /v1/auth/developer/otp/request.
  • 409 EMAIL_EXISTS — another account owns this mailbox under an alias.

Curl:

curl -X POST "${BASE_URL}/auth/creator/verify" \
  -H 'Content-Type: application/json' \
  -d '{"email":"sam@example.com","code":"123456","display_name":"Sam Rivera"}'