<!-- Machine-readable twin of https://repoint.studio/embed. Generated at build time; do not edit. -->

> Put Repoint Studio’s QR designer inside your own product: an iframe that posts the finished code back to your page, or a handoff that returns it to your server. Free, no key, no account.

Canonical HTML: https://repoint.studio/embed
Site index for agents: https://repoint.studio/llms.txt

---

For developers

# Embed a QR Code Generator in Your Site

Your product needs a QR code. You could write an encoder in an afternoon; what you cannot write in an afternoon is the designer, the preflight and the eleven content formats. So borrow ours — put Studio inside your page, or hand the visitor over and take the finished code back. It is free, there is no key and no account, and nothing anyone types passes through a server of ours.

## Two ways in

**Embed** puts the designer in an iframe on your own page and posts the result back to your JavaScript. The visitor never leaves you. Use this one unless something stops you.

**Handoff** sends the visitor to repoint.studio with a `return_to`, and returns them to you with the code. Use it when a content-security policy forbids framing, when you are inside an app webview, or when the designer would not fit the page you have.

Either way the visitor gets a designer with your content already in it, no download button unless you asked for one, and a Done button wearing whatever words you chose.

## The content is yours, not theirs

Every integration sends a `type` and the fields that type needs, and both are mandatory: a designer with nothing to encode refuses to draw rather than showing an empty box nobody is allowed to fill. “Enough” is the encoder’s own answer, not a rule of ours — a `mailto:` with no recipient and a payment with no IBAN are as empty as no fields at all, and both are refused. You already know what the code has to say — this product’s link, this table’s WiFi, this invoice — and what you are short of is the designer.

So the visitor cannot change it, and is not teased with the option. The type you chose is the only one on screen; the fields under it are read-only. What they get is the part you did not want to build: shape, colours, corner frames, a centre mark from a library of 128, error correction, and a live preflight that says whether the thing will actually scan. The code that comes back encodes exactly the string you asked for.

## The designer itself

Read-only, and running inside this page. What your visitor gets is this with your content already in it, a Done button wearing your words, and nothing else added.

This copy is inert by construction: `mode=view` renders a design that was already made and refuses every control. [Open it on its own](/embed/app?mode=view&d=eyJ0eXBlIjoidXJsIiwiZiI6eyJ1cmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9wLzEifSwic2hhcGUiOiJkb3RzIiwiZnJhbWUiOiJyb3VuZGVkIiwiZG90IjoiY2lyY2xlIiwiZmciOiIjNWI1Y2UyIiwibWFyZ2luIjo0fQ).

## Embed it

Three short blocks, and every value in them is real. Paste them, change the two lines that say what your code contains, and it works.

### 1. Put the frame on your page

```
<iframe id="qr" title="QR code designer"
        style="width:100%;height:900px;border:0"></iframe>
```

### 2. Point it at Studio

```
const STUDIO = 'https://repoint.studio'

const url = new URL(STUDIO + '/embed/app')
url.search = new URLSearchParams({
  type: 'url',                           // which of the eleven kinds of code
  'f.url': 'https://acme.example/p/1',   // what it encodes — yours, and they cannot change it
  label: 'Add to product',               // the words on the button they press when done
})

document.getElementById('qr').src = url  // URLSearchParams does the escaping for you
```

Setting `src` from a `URL` is what saves you writing `f.url=https%3A%2F%2Facme.example%2Fp%2F1` by hand. You can of course put the finished address straight in the HTML instead — the escaping is the only reason not to.

### 3. Take the result

```
addEventListener('message', (e) => {
  if (e.origin !== STUDIO) return          // the only check that matters
  const m = e.data
  if (!m || typeof m !== 'object') return

  // The designer grew or shrank. Match it and nothing clips.
  if (m.t === 'repoint.resize') {
    document.getElementById('qr').style.height = m.h + 'px'
  }

  // They pressed Add to product. This can happen more than once.
  if (m.t === 'repoint.done') {
    m.payload.text   // 'https://acme.example/p/1' — exactly what a scanner will read
    m.svg            // '<svg viewBox="0 0 …">…</svg>' — the artwork
    m.token          // 'eyJ0eXBlIjoidXJs…' — the design, if you want to reopen it later
  }
})
```

That is the whole integration. `repoint.done` arrives every time the visitor presses the button, so a second press replaces the first rather than adding to it.

### When the content will not fit in a URL

A vCard with a biography in it runs past what an address bar will take. Every parameter can travel in a `repoint.init` message instead, under the same names: the frame announces itself with `repoint.ready`, you answer, and step 3 is unchanged.

```
addEventListener('message', (e) => {
  if (e.origin !== STUDIO) return
  if (e.data?.t !== 'repoint.ready') return

  // The same names the query string takes. Reply to e.source, which is the frame's window.
  e.source.postMessage({ v: 1, t: 'repoint.init', state: sessionId, config: {
    type: 'vcard',
    'f.name': 'Dana Okoye',
    'f.org': 'Acme Industrial',
    'f.note': longBiography,
  } }, STUDIO)
})
```

This is also how the frame learns which origin it may reply to, so a host that sends `init` never needs the `origin` parameter.

## Hand off instead

A link out to Studio and an endpoint of yours to catch the visitor coming back. Two blocks, one on each side.

### 1. Build the link

```
// Wherever you render the page that carries the link.
session.qrState = crypto.randomUUID()          // your nonce
session.qrText = 'https://acme.example/p/1'    // what you are asking for

const url = new URL('https://repoint.studio/embed/app')
url.search = new URLSearchParams({
  type: 'url',
  'f.url': session.qrText,
  return_to: 'https://acme.example/qr/callback',  // they see and confirm this address
  state: session.qrState,                         // comes back untouched
  label: 'Send it to Acme',
})

// <a href="${url}">Design a QR code</a>
```

### 2. Catch the return

```
// The visitor arrives back by POST, as a normal form submission.
app.post('/qr/callback', express.urlencoded({ extended: false }), (req, res) => {
  const { repoint, state } = req.body

  // Do NOT check the Origin header: Studio may send Origin: null, and a cross-site form
  // POST proves nothing anyway. The nonce you issued is what ties this to the session.
  if (state !== req.session.qrState) return res.sendStatus(400)

  const result = JSON.parse(repoint)
  if (result.v !== 1) return res.sendStatus(400)
  if (result.payload.text !== req.session.qrText) return res.sendStatus(400)

  saveSvg(req.session.productId, result.svg)   // render it as an <img>, never inline
  res.redirect('/products/1')
})
```

A POST is the default because artwork does not fit in a query string. Add `return: 'get'` and the visitor is redirected back with the design token and no artwork, which is enough if you only mean to store it and reopen it later.

## Parameters

Contract version 1. Additions will not break a deployed integration; anything that could would arrive with a new version number.

| Name | Values | What it does |
| --- | --- | --- |
| `v` | 1 | Contract version. Omit and you get the current one; pin it and a future change cannot surprise you. |
| `mode` | embed \| handoff \| view | Usually inferred — framed is embed, top-level with a return_to is handoff. view renders a stored design read-only. |
| `type` | url \| text \| wifi \| email \| phone \| sms \| vcard \| geo \| event \| wa \| pay | Required. Which kind of code this is. It is the only type the visitor sees, and it cannot be changed. |
| `f.<field>` | string | Required. The content, named as in the type reference: f.url, f.ssid, f.name. Enough of them to actually make the code — a mailto with no recipient or a payment with no IBAN is refused, not drawn empty. Shown read-only, never hidden; an unknown name is reported, not ignored. |
| `d` | design token | A starting design — the same token the Copy design link button emits. Explicit type and f. parameters win over it. |
| `allow` | export, copy, link | Off by default in both modes: the result goes back to you, not into the visitor’s downloads. |
| `out` | svg, png | What comes back with the result. Defaults to svg. |
| `px` | 1024 \| 2048 \| 4096 | Pixel size when png is requested. |
| `state` | string | Echoed back untouched, up to 512 characters. Tie the return to the session that started it. |
| `return_to` | https URL | Handoff only. Where the visitor goes when they are done — they see and confirm this address first. |
| `return` | post \| get | post (default) submits the result as a form. get redirects with a token only and no artwork. |
| `origin` | https origin | Embed only, and optional: send a repoint.init message instead and the frame learns your origin from it. |
| `label` | string | The words on the Done button, up to 40 characters. |
| `theme` | light \| dark \| auto | Defaults to light, which is what the tool itself uses. |

Field names per type — `f.url`, `f.ssid`, `f.name` — are listed on [the type pages](/qr-code-types), one page per kind of code.

## What comes back

```
{
  "v": 1,
  "t": "repoint.done",
  "state": "the string you sent, untouched",
  "payload": { "type": "url", "text": "https://acme.example/p/1" },
  "design": { "type": "url", "f": { … }, "shape": "dots", … },
  "token": "eyJ0eXBlIjoidXJsIiw…",
  "svg": "<svg viewBox=\"0 0 …\">…</svg>",
  "png": "data:image/png;base64,…",
  "meta": { "version": 2, "ec": "M", "mm": 38, "quietZone": 4, "passed": 6, "checks": 6 }
}
```

| Field | What it is |
| --- | --- |
| `payload.text` | Exactly what a scanner will read. Compare it with what you asked for — that is the check that proves you got the right code, and it needs no image decoding. |
| `design, token` | The design as an object, and the same thing as one URL-safe string. Hand the token back to /embed/app?d=… to reopen it, or with mode=view to render it read-only. |
| `svg` | The artwork, when you asked for out=svg. Tens of kilobytes with a centre mark, which is why a handoff posts rather than redirects. |
| `png` | A data URL, when you asked for out=png. px picks 1024, 2048 or 4096. |
| `meta` | QR version, error-correction level, print size in millimetres, quiet zone, and how many preflight checks passed out of how many. |
| `state` | Whatever you sent, echoed back untouched. Studio never looks inside it. |

## The messages

| Message | Direction | Meaning |
| --- | --- | --- |
| `repoint.ready` | frame → you | The designer has loaded. It carries no data, which is the only reason it may be sent to *, and it is your cue to send init. |
| `repoint.init` | you → frame | Configure it, using the same names the query string uses. This is also how the frame learns which origin it may reply to, so everything after this goes to you and nowhere else. |
| `repoint.resize` | frame → you | The content height changed. Set the iframe to m.h and nothing clips. |
| `repoint.done` | frame → you | A finished code. An embed is not one-shot — the visitor can change something and press again, and you get a second one carrying the same state. |
| `repoint.cancel` | frame → you | The visitor backed out of a confirm screen. |

## Four things to get right

- Render the SVG as an image, not as markup.An `<img src="data:image/svg+xml;base64,…">` executes nothing. Inlining SVG that arrived over the network is the mistake this whole category of integration keeps making.
- Do not check the Origin header on a handoff.Studio may send `Origin: null`, and a cross-site POST could have come from anywhere in any case. Your state nonce is the tie back to the session; the content is the visitor’s input either way, so validate it.
- Check payload.text against what you asked for.The visitor could not have changed it — the content is locked in every mode — but this is the one field that says exactly what a scanner will read, and the check costs a line.
- The visitor confirms before a handoff returns.Studio shows them the site it is about to send them to and waits for a click. That is deliberate: it is what lets anyone integrate without registering with us, and it is not something to design around.

## What it costs

Nothing, and there is nothing to sign. The designer carries a small “Made with Repoint Studio” line, and that stays — it is the only thing this earns. If it saved you an afternoon, [buy us a coffee](https://buymeacoffee.com/peyman.goldasteh); a link to [repoint.studio](/) from your own page is worth more to us than one inside the frame.

Nothing a visitor types reaches us. When a code goes back to you, Studio records the same settings-only note it records for a download, plus whether it was embedded or handed off and your domain, so we know which integrations are alive. Never the content. [Privacy](/privacy) lists every field.
