QR Codes

Render a QR code for any short link, or encode a URL, text, WiFi network, v-card, email, phone number or SMS directly. Both endpoints always emit at least the 4-module quiet zone the standard requires.

Which endpoint to use

There are two, and the difference matters more than it looks.

GET /api/links/{id}/qr for a code you can change later

Encodes the short link, so the destination stays yours to repoint after the code is printed, and every scan is counted as a click. This is what people mean by a dynamic QR code, and it is almost always the one you want for anything that goes on paper.

POST /api/qr for content that is not a link

Encodes the content itself. Nothing is stored, nothing is tracked, and the code cannot be changed once it exists. That is the right trade for a WiFi password or a contact card, which have no destination to repoint, and the wrong trade for a campaign URL.

QR code for a short link

Returns a PNG data URL that encodes the link's short URL. Every render option below is optional.

Request
curl "https://www.flyn.to/api/links/LINK_ID/qr?size=1024&level=H" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response
{
  "shortUrl": "https://flyn.to/spring-promo",
  "qrDataUrl": "data:image/png;base64,iVBORw0KGgo...",
  "qr": {
    "size": 984,
    "margin": 4,
    "level": "H",
    "fg": "#14532D",
    "bg": "#FFFFFF",
    "format": "png",
    "moduleSize": 24,
    "symbolModules": 33
  }
}

QR code for any content

Set type and the fields that type needs. Link, Text and WiFi work on every plan; V-card, E-mail, Call and SMS require Flyn Pro.

Request
curl -X POST https://www.flyn.to/api/qr \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "wifi",
    "wifiSsid": "Cafe Guest",
    "wifiPass": "flatwhite2026",
    "wifiEnc": "WPA"
  }'
Response
{
  "type": "wifi",
  "label": "WiFi: Cafe Guest",
  "payload": "WIFI:T:WPA;S:Cafe Guest;P:flatwhite2026;;",
  "qrDataUrl": "data:image/png;base64,iVBORw0KGgo...",
  "qr": {
    "size": 481,
    "margin": 4,
    "level": "M",
    "fg": "#14532D",
    "bg": "#FFFFFF",
    "format": "png",
    "moduleSize": 13,
    "symbolModules": 29
  }
}

Content types and their fields

typePlanFields
urlFreeurl
textFreetext
wifiFreewifiSsid (required), wifiPass, wifiEnc, wifiHidden
vcardProvcName (required), vcOrg, vcPhone, vcEmail, vcUrl
emailProemail (required), emailSubject, emailBody
phoneProphone
smsProsmsPhone (required), smsBody

Phone numbers need a country code

A phone, sms or vcard number without a leading + and country code returns a warnings array alongside the image. The code is still generated, because a number that works domestically is better than no code at all, but it will fail silently for anyone scanning it from another country. That is not something you can fix after it is printed.

Render options

These apply to both endpoints. Send them as query parameters on GET /api/links/{id}/qr and as body fields on POST /api/qr.

FieldTypeDescription
formatstring'png' (default) or 'svg'. SVG requires Flyn Pro. Both formats are returned as qrDataUrl; SVG additionally returns raw markup in svg.
sizeintegerTarget width in pixels, 64 to 2048, default 512. Not an exact width: a QR is a grid, so the image is the largest whole-module square that fits, and the real width depends on how much data the payload needs. A short link at the 512 default lands around 480 to 500. Read qr.size for the actual width and qr.moduleSize for the pixels per module. One exception in the other direction: if the payload is dense enough that even one pixel per module would exceed the target, one pixel per module wins and the image comes back larger than requested, because an over-wide QR is recoverable and a sub-pixel one is not.
marginintegerQuiet zone in modules, default and minimum 4, maximum 32. Values below 4 are raised to 4. ISO/IEC 18004 requires four blank modules on every side, and a short quiet zone is the single most common reason a printed code will not scan.
levelstringError correction level: 'L', 'M' (default), 'Q' or 'H'. Higher levels survive more damage but produce a denser symbol that needs more physical space to stay readable.
fgstringForeground colour as a 6-digit hex string, default Flyn green #14532D. Requires Flyn Pro. Keep it dark: QR scanners expect dark modules on a light background.
bgstringBackground colour as a 6-digit hex string, default #FFFFFF. Requires Flyn Pro.

Why the size you get back is not the size you asked for

A QR code is a grid, and every cell in that grid has to be the same number of pixels wide. If 512 pixels does not divide evenly by the grid size, some cells come out a pixel wider than others, and those ragged edges are what a camera at an angle fails to read. Flyn snaps the image down to the largest whole-module square instead, and reports the real width in qr.size and the pixels per module in qr.moduleSize. How far it lands from your number depends on the payload, because a longer payload needs a denser grid: the same 512 request returns 481 for a WiFi code and 495 for a short link.

Response fields

FieldTypeDescription
qrDataUrlstringThe image as a data URL, ready to drop into an img tag, an email, or a PDF. Always present, for both PNG and SVG.
svgstringRaw SVG markup. Only present when format is svg.
shortUrlstringThe encoded short URL. Only on GET /api/links/{id}/qr.
typestringThe content type that was encoded. Only on POST /api/qr.
labelstringA short human-readable summary of what was encoded, such as "WiFi: Cafe Guest". Handy as a filename or a history entry. Only on POST /api/qr.
payloadstringThe exact string encoded in the symbol. Useful for verifying what a scanner will actually see. Only on POST /api/qr.
qrobjectThe options actually applied after clamping, plus moduleSize and symbolModules. Read this rather than assuming your requested values were used verbatim.
warningsstring[]Non-blocking advisories, such as a phone number with no country code. Only present when there is something to say.

Errors

400

A malformed option or a missing required field. The message names the exact field, for example a wifi code sent without wifiSsid.

403 with code: "UPGRADE_REQUIRED"

A Pro-only content type, custom colours, or SVG output on a free plan. The response carries upgradeUrl.

404

The link id does not exist, or it belongs to another account. Only on GET /api/links/{id}/qr.

429

Both QR endpoints allow 60 requests per minute per IP. See Rate Limits.

Through an AI assistant

Both endpoints are exposed as MCP tools, get_link_qr_code and create_qr_code, so an assistant can generate a code and show it to you in the conversation. See Flyn for AI assistants.

Was this page helpful? Spotted something wrong?