Skip to main content

QR code generation model

Orbit’s QR surface (/api/v1/qr) turns a payload into a scannable image, rendered in-cluster. The QR Code API reference holds the parameter shapes and request examples; this page names the model behind it — what each endpoint mints, why tenant-supplied payloads pass through a safety filter before they become an image, and when a QR code is the wrong tool because you actually wanted click tracking.

What the surface mints

Three GET endpoints share the same rendering pipeline and differ only in what they encode:
  • GET /api/v1/qr/generate encodes an arbitrary data payload — plain text or a URL — as-is. Use it when the payload is already the final string you want a scanner to receive.
  • GET /api/v1/qr/whatsapp takes a phone number and an optional message, and encodes a https://wa.me/<digits> click-to-chat deep link. Scanning opens a WhatsApp chat with that number, pre-filled with the message. The encoded URL carries only the digits from the phone number.
  • GET /api/v1/qr/sms takes a phone number and an optional message, and encodes an sms:<E.164>?body=… URI. Scanning opens the device’s native SMS composer, addressed and pre-filled. The phone is normalized to E.164 before it goes into the URI.
All three require authentication (API key or session JWT) and sit on the authenticated-read rate limit. There is no POST body — every input arrives as a query parameter, so a QR render is safe to embed as a plain URL wherever you can put an image URL with credentials.

The safety model

A QR code is a payload you ask a stranger’s phone to trust. Two facts about the scanners that read them drive the design:
  • Scanner apps preview the encoded string as a tappable link, and a small fraction execute javascript: or data: URIs in a hosted webview — so the payload itself must be filtered, not just rendered.
  • Everything you send is encoded verbatim into the image. A filter that only affects the HTTP response would change nothing; the filter has to run before encoding.
The surface applies two kinds of guard, both in the request path before any pixels exist: Scheme filtering on /generate. Requested payloads are rejected with a 400 when they begin with a script-execution or non-network URL scheme — javascript:, data:, vbscript:, file:, about:, or blob:. Plain text, http(s), mailto:, tel:, sms:, and wa.me links pass through. The check exists so a payload that came from an untrusted source (a form field, an import, a customer-supplied value) cannot ride your authenticated QR render into a scanner’s browser. Two-stage phone validation on /whatsapp and /sms. The phone parameter passes through two independent gates:
  1. A shape floor — the input may contain only characters that legitimately appear in a human-typed international number (+, digits, spaces, parentheses, dashes, dots), between 4 and 32 characters. Any letter, colon, or angle bracket fails with a validation error. This blocks the XSS-shaped payloads (phone=javascript:…) before they reach the URI assembly.
  2. A dialability check — the surviving input is parsed by a full international phone library and must normalize to a real, dialable E.164 number. Valid-shape but undialable inputs (+12, all zeros, a country code with no subscriber digits) fail here with INVALID_PHONE.
The two stages answer different questions: the floor says “this string could never be anything but a phone number”; the parser says “this phone number actually dials.” The SMS route then encodes the normalized E.164 string, not the raw input, so the sms: URI contains only a leading + and digits regardless of how the caller formatted the number. The WhatsApp route keeps only digits, matching the wa.me contract. Below the guards, rendering itself is local: the PNG is drawn in-cluster by the API process. No payload — phone number, message body, or arbitrary URL — leaves the platform to a third-party image service.

Encoding rules

The rendering parameters are uniform across all three endpoints: PNG is the default because the common case is “put this in a template”; the JSON envelope exists for callers that need the image as a value rather than a stream.

Error vocabulary

Malformed query strings can also surface as the standard 422 parse envelope, in line with the rest of the API. A generated QR is an untracked render. Nothing is recorded when it is scanned: there is no click row, no analytics rollup, no attribution, no webhook — you get back an image and that is the whole transaction. That is the right shape for point-of-sale posters, packaging, and badges where the scan happens on hardware you do not control and there is nothing to attribute. When you need the scan to be measurable — per-recipient attribution, campaign rollups, a short_link.click event — mint a short link and encode its public URL (/l/:code) into the QR via /generate. The QR becomes the print-facing transport for a trackable redirect: every scan flows through the link’s click pipeline, while the image itself stays a dumb render. Use QR alone when anonymity and zero bookkeeping are the feature; QR-over-short-link when the campaign needs the scan counted.

See also