> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# QR code generation model

> The three GET endpoints on /api/v1/qr — generate, whatsapp, and sms — the safety model that filters tenant-supplied payloads and double-validates phone numbers before encoding, the encoding rules (size bounds, byte cap, response formats), the error vocabulary, and when an untracked QR render is the right tool versus a trackable short link.

# 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](/api-reference/qr) 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:

| Rule                   | Value                                                                                                                                                                                                                                |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `size`                 | 50–1024 pixels per edge, default 300. The output PNG is always square.                                                                                                                                                               |
| Payload length         | `data` on `/generate` is capped at 2048 characters; `message` on the deep-link routes at 1024.                                                                                                                                       |
| Error correction       | Level M with a 2-module quiet margin — tuned for print and kiosk scanning, not maximum density.                                                                                                                                      |
| `format=png` (default) | Raw `image/png` bytes with a `Cache-Control: private, max-age=300` header. Ready for an `<img>` tag or a print template.                                                                                                             |
| `format=json`          | The standard `{ data, meta }` envelope with `data.qr_data_url` — a base64 data URL you can store or hand to a client renderer. The deep-link routes add the resolved link (`whatsapp_link` / `sms_link`) and the normalized `phone`. |

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

| Code               | Status | Raised when                                                                                                                                                                                          |
| ------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `VALIDATION_ERROR` | 400    | A query parameter fails schema validation (missing `data`, out-of-range `size`, oversize payload), the `data` payload opens with a forbidden URL scheme, or `phone` fails the character-shape floor. |
| `INVALID_PHONE`    | 400    | `phone` passed the shape floor but does not parse as a dialable international number.                                                                                                                |
| `QR_RENDER_FAILED` | 500    | The in-cluster PNG renderer itself failed — a platform error, not an input problem. Retry; if it persists, treat it as an incident.                                                                  |

Malformed query strings can also surface as the standard `422` parse envelope, in line with the rest of the API.

## QR codes vs. short links

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](/concepts/short-links) 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

* [QR Code API reference](/api-reference/qr) — parameters, response envelopes, and request examples.
* [Short links, landing pages, and the publish lifecycle](/concepts/short-links) — the trackable artifact family to pair with QR codes when attribution matters.
* [Public pixels and redirects](/concepts/public-pixels-redirects) — the anonymous, rate-limited edge those public URLs are served from.
