Reticle

Reticle QR Code API

Authentication

Every request is authenticated with a bearer API key, minted from your account page. A key is shown to you exactly once at creation time; Reticle stores only a hash of it, never the key itself, so if you lose it there's nothing to recover, only a new key to mint. Each account can hold a small number of named keys at once (see the limits table below) and revoke any of them individually at any time; a revoked key stops working on its very next request.

Send the key as Authorization: Bearer rk_live_<your key>. The API is server-to-server by design: it sends no CORS headers, so a browser page cannot call it directly, which is deliberate, since a page that could would be shipping your bearer key to anyone who opens dev tools. Call it from a server, a script, or a scheduled job instead.

Any request with a body must send valid JSON with Content-Type: application/json. A malformed body (invalid JSON) is rejected with a plain 400 before it reaches the API's own logic. The wrong content type still reaches the handler, which reports it with the same JSON error shape used everywhere else, described below.

Quick start

Create a dynamic link that redirects to a destination URL:

curl -X POST https://reticle.cloud/api/v1/links \
  -H "Authorization: Bearer rk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"destination": "https://example.com/menu", "name": "menu"}'

which returns:

{
  "slug": "aB3xY9",
  "shortUrl": "https://open-qr.link/aB3xY9",
  "kind": "redirect",
  "destination": "https://example.com/menu",
  "name": "menu",
  "active": true,
  "labels": [],
  "customSlug": null,
  "linkDomain": null,
  "createdAt": "2026-08-12T19:04:05.000Z",
  "scanCount": 0,
  "lastScanAt": null,
  "reclaimed": false,
  "reclaimedAt": null
}

That response shape, the "public link" shape, is what every endpoint below returns for a link: a fixed, closed set of fields. Anything not in that list (routing rules, password hashes, Safe Browsing verdicts, the owner id) is never returned by the API, no matter what's on the underlying document. Timestamps are always ISO 8601 UTC strings, or null when not yet set.

Endpoints

All six routes live under https://reticle.cloud/api/v1:

Method + pathWhat it does
POST /linksCreate a new redirect link from { destination, name?, linkDomain? }. linkDomain (optional string): one of your active custom domains; omitted means your default domain. Send the bare hostname exactly as it appears on your Account page, with no scheme or path (for example go.acme.com).
GET /linksList your links, ordered by slug, not by creation date. Paginate with ?limit= (default 100, max 500) and ?cursor=.
GET /links/{slug}Read one link you own.
PATCH /links/{slug}Update destination, name, and/or active. Any other field in the body is rejected.
DELETE /links/{slug}Delete a link and its scan history.
GET /links/{slug}/statsAggregate scan stats: total count, last scan time, and per-day/device/hour/country breakdowns.

Reading works for every kind of link: GET /links/{slug} and its /stats return a link's public shape or its stats whether the link is a redirect, a file link, a page, or a dynamic vCard. Writing is redirect-only: PATCH and DELETE both return wrong_kind on any link that isn't a plain redirect. A slug that doesn't exist, or belongs to a different account, always returns not_found, never a permission error, so the API never confirms whether someone else's link exists.

Errors

Every error response is JSON, in the same shape:

{ "error": { "code": "rate_limited", "message": "Too many requests. Please slow down and try again." } }
CodeStatusMeaning
unauthorized401The API key is missing, invalid, or revoked.
plan_required403The account is not on an active Pro plan, or (on create) its email is not yet verified.
not_found404No such link, or the slug belongs to a different account.
method_not_allowed405That HTTP method isn't supported on this path.
wrong_kind409This link isn't a redirect link, so it can't be patched or deleted through the API (reading it, and reading its stats, still work).
invalid_request422The request body is missing a required field, has the wrong type, or includes an unknown field.
link_limit_reached422The account has reached its dynamic link limit.
unsafe_destination422The destination URL failed a safety check.
domain_not_active422That domain isn't active on this account.
rate_limited429Too many requests; the response includes a Retry-After header.
internal500Something went wrong on our end. Please try again.

Rate limits

Two ceilings apply, and they're separate budgets: an overall request ceiling that covers every call you make, and a smaller creation budget that link creation shares with the ordinary create-a-link flow in the app itself (so a script and a signed-in session drawing from the same account draw from the same creation budget). Both fail open: if the rate limiter itself has trouble, your request still goes through rather than getting refused by an outage on our side. A request that's over a limit gets back 429 rate_limited with a Retry-After header telling you how many seconds to wait.

LimitAmount
API keys per account5
Requests per hour, all endpoints combined3600
Requests per day, all endpoints combined20000
Link creations per hour250
Link creations per day400

Webhooks

A webhook sends every scan to a system of yours the moment it happens. Add up to three HTTPS endpoints on your account page and Reticle POSTs one JSON event per scan, on every dynamic link the account owns. Events usually arrive within a few seconds. The first event after adding or changing an endpoint can take up to a minute. Webhooks are part of Reticle Pro.

An event is sent for every scan that Reticle records in your per-scan analytics, and only those. That means dynamic links on a Pro account, with crawler and link-preview fetches excluded: those still count toward a link's scan total, but no person looked at the code, so no event is sent. If your account leaves Pro, per-scan records stop and so do webhooks.

The event

{
  "id": "k3Yx9QpB2tLm7VcN4wRz",
  "type": "scan",
  "createdAt": "2026-08-29T17:03:12.412Z",
  "link": { "slug": "ab3Kq9", "name": "Spring menu", "kind": "redirect", "url": "https://open-qr.link/ab3Kq9" },
  "scan": {
    "at": "2026-08-29T17:03:12.104Z",
    "device": "mobile", "os": "iOS 17", "browser": "Safari",
    "country": "US", "region": "California", "city": "Oakland", "lat": 37.8, "lon": -122.27,
    "target": "destination", "referrer": "instagram.com", "returning": false
  }
}

Every field is present on every event; a value we do not have is null. Delivery is at least once, so treat a repeated id as the same scan. The event never includes the scanner's IP address or user agent; Reticle does not keep them.

FieldTypeWhat it holds
idstringThe scan record's id, 20 characters. Use it as your deduplication key.
typestringscan for a real scan, or test for the sample event the Test button on your account page sends.
createdAtstringISO 8601 UTC. When Reticle built the event. Compare it with scan.at to measure delivery lag.
link.slugstringThe link's short code.
link.namestring or nullThe name you gave the link, or null if it has none.
link.kindstringOne of redirect, vcard, file, bio.
link.urlstringThe link's public URL, on your own custom domain when it has one.
scan.atstringISO 8601 UTC. When the scan was recorded.
scan.devicestringOne of mobile, tablet, desktop, other.
scan.osstring or nullOperating system family with a major version where the device states one, for example iOS 17, Android 14, macOS, Windows, Linux.
scan.browserstring or nullOne of Safari, Chrome, Firefox, Edge, Samsung, Instagram, Facebook, TikTok, Snapchat, LinkedIn, WeChat, Other.
scan.countrystring or nullISO 3166-1 alpha-2 country code, resolved at the CDN edge. null when the country could not be resolved.
scan.regionstring or nullState or region name. null when unknown.
scan.citystring or nullCity name. null when unknown.
scan.latnumber or nullApproximate latitude from our geolocation provider, city level, never a device GPS reading. null when unknown.
scan.lonnumber or nullApproximate longitude, same source and precision as lat. null when unknown.
scan.targetstringWhat was served. destination: the link's usual destination (also what a page or vCard link reports, since its hosted page is the destination). route: a smart-routing rule matched. ios: the iOS override destination. android: the Android override. file: a file link served its stored file.
scan.referrerstring or nullThe referring site's host only, never the full URL, for example instagram.com. null when the scan arrived with no referrer, which is the normal case for a camera scan.
scan.returningbooleantrue when this visitor has scanned one of your links before.

Headers and signature

Each request carries Reticle-Event-Id, Reticle-Event-Type, and Reticle-Signature in the form t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 over <t>.<raw body> using the endpoint's signing secret. Verify it against the raw request bytes, compare in constant time, and reject timestamps more than five minutes old:

const crypto = require("node:crypto");
function verify(secret, rawBody, header) {
  const m = /^t=(\d+),v1=([0-9a-f]+)$/.exec(header || "");
  if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(m[1] + "." + rawBody).digest("hex");
  return expected.length === m[2].length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
}

Responding

Answer with any 2xx status within 3 seconds and do the real work afterwards. Redirects are not followed and count as failures. After 20 failed deliveries in a row the endpoint is paused and you get one email; fix the receiver and press Resume on your account page. Deliveries are not retried. An account can send up to 1000 scans per hour to its endpoints (up to 3000 deliveries across three endpoints).

Zapier and Make

In Zapier, start a Zap with Webhooks by Zapier, trigger Catch Hook, and paste the URL it gives you as a Reticle endpoint. In Make, add a Webhooks module, Custom webhook, and do the same. Both accept events without checking the signature, so verification there is optional.

Frequently asked questions

Is the API included in Pro?

Yes. The API is included with Reticle Pro at no extra cost, whether you're on the $7 a month or $60 a year plan. There is no separate API tier and no metered usage billing on top of the subscription.

Can I create file or bio links through the API?

No. Version 1 of the API creates and manages ordinary redirect links only. File QR codes, pages, and dynamic vCards stay UI-only for now; they may be added to a future API version.

What happens to my links if I stop using the API?

Nothing. Links and QR codes you created through the API keep working and keep resolving. If your account leaves Pro, a link that then records no scans for 90 days becomes eligible for inactivity reclaim like any free link, after email warnings. Printed codes are never tied to ongoing API use.

Is there a free API tier?

No. The QR code generator and static codes are free with no account required, but the API itself requires an active Reticle Pro plan; there is no free tier or trial for API access specifically.

Ready to script your codes? Upgrade to Reticle Pro, then open your account page to mint your first API key.