Design tools
Design tool integrations
The QR Hero plugins for Figma and Canva talk to a small, separate API at
/integrations/v1. It exists because a plugin cannot keep a
client secret and has no redirect URI to come back to, so it authenticates with the
OAuth 2.0 Device Authorization Grant
(RFC 8628)
instead of a normal OAuth handshake.
This page documents that surface in full. If you are here because your security review asked what a design plugin can reach inside your organization, the answer is on this page and nothing is omitted.
How it differs from the public API
/integrations/v1 is not the metered
/v1 API and does not behave like it.
- Not billed. A design panel searches and re-renders constantly while somebody nudges a layout. Charging a wallet per keystroke would make the plugin expensive to use, so these calls are throttled (120/min per connection) instead of metered.
- CORS-open, bearer-only. A plugin iframe has a
nullorigin, so these routes sendAccess-Control-Allow-Origin: *. They are never cookie-authenticated, and credentials are disabled — the wildcard cannot be turned into session-riding CSRF. - Different tokens. A token issued by pairing works here; an ordinary API token does not. That is deliberate, so this surface cannot be used to opt out of API metering.
- Narrower payloads. Code objects returned here omit routing rules, tracking pixels, and custom JavaScript. A design tool has no use for them, and sending them into a third-party editor would be gratuitous exposure.
The pairing flow
- 1. The plugin calls
POST /pairingand receives adevice_code(secret, never shown), auser_code(short, displayed), and a verification URL. - 2. It opens that URL in a real browser. The person lands on the consent page in a normal signed-in session.
- 3. They check the code matches, choose which organization the plugin may see, and approve.
- 4. The plugin polls
POST /pairing/tokenevery 3s until a token comes back.
A user code is valid for 10 minutes. The token is handed over
exactly once — a second poll after a successful claim returns
invalid_grant, so two plugin windows on one pairing cannot both
walk away with a credential.
One deliberate deviation worth knowing: expiry bounds the waiting window, not the grant. If somebody approves seconds before the code expires, the token is still delivered. The alternative would leave a live token nobody can reach and nobody knows to revoke.
Poll responses
Branch on the error string, not the HTTP status — the status is
a hint, the string is the contract.
| error | HTTP | What it means |
|---|---|---|
authorization_pending |
202 | Nobody has approved yet. Keep polling. |
slow_down |
202 | You polled faster than the interval. Widen it and keep going — this does not count against the pairing. |
access_denied |
403 | The person declined. Stop. |
expired_token |
410 | The code timed out before approval. Start a new pairing. |
invalid_grant |
400 | Unknown device code, or the token was already claimed. Start a new pairing. |
Endpoints
Base URL https://api.qrhero.com/integrations/v1. Send
Authorization: Bearer <token> and
Accept: application/json — including on the endpoints that
return image bytes, or errors come back as redirects rather than JSON.
| Endpoint | Requires | Description |
|---|---|---|
POST /pairing
|
none
|
Start a pairing. Returns a device code, a short user code, and the URL to send the person to. |
POST /pairing/token
|
device code
|
Poll for the result. Returns the access token once a human has approved. |
POST /render
|
none
|
Render a static QR code as PNG or SVG. Nothing is stored and nothing is measurable. |
GET /me
|
token
|
The connected organization, the signed-in user, and what this connection is allowed to do. |
GET /codes
|
codes.view
|
Search and page through the organization's QR codes. |
GET /codes/{ulid}
|
codes.view
|
A single code. |
GET /codes/{ulid}/image
|
codes.view
|
Rendered bytes for a code — PNG or SVG. Add ?as=url for a short-lived signed URL instead. |
POST /codes
|
codes.create
|
Create a dynamic code from a destination URL, optionally with a saved style. |
GET /domains
|
domains.view
|
Short domains available for new codes. |
GET /styles
|
styles.manage
|
The organization's saved QR styles, so a code can be created on-brand. |
What a connection can and cannot do
Approving a pairing issues an organization-scoped token requesting these abilities:
codes.view
codes.create
styles.manage
domains.view
brands.view
They are filtered against the approving user's own role, so a connection can never grant more than the person who approved it already had. Someone with read-only access produces a read-only connection, and the plugin degrades to inserting existing codes rather than erroring.
A connection cannot read scan analytics, billing, team members, or account details. There is no ability in the list above that grants any of them.
Revoking
Each connection is backed by an ordinary API token named after the plugin, for example
Figma plugin (a1b2c3). Delete it from
Settings → API tokens and the
connection stops working immediately. Revoking the token and revoking the plugin are the
same action, on purpose — there is no second place to look.
Free static rendering
POST /render needs no account and returns a
static QR code: the destination is baked into the modules. It cannot be
re-pointed after the artwork is printed and it reports nothing, so if the link ever changes
or breaks, every printed copy is dead. Responses carry
X-QR-Hero-Code-Type: static so a client cannot present it as
equivalent to a dynamic code by accident.
It is rate-limited to 20/min per IP, capped in payload length, and accepts no logo input.