API for developers

Connect qsy to the systems you already run.

A REST API that speaks JSON. Your POS, booking system or online shop creates short links with QR codes, changes where they point and reads scan counts. The API comes with Business.

For integrators

Built for systems that own a lot of codes.

Picture a restaurant system with one QR code per table across twenty restaurants. This is how it works with qsy:

  • One folder and one key per restaurant. A key can be locked to a single folder. It only sees its own codes, so a bug in one integration never reaches the others.
  • Your ids, not ours. Give every code your own external_id, such as the table number in the POS. One PUT call creates the code if it is missing and updates it if it exists.
  • Safe to retry. Send an Idempotency-Key when you create. If the network drops mid-call, a retry returns the same response instead of a duplicate.
  • The printed code stays put. Change the destination as often as you like. The slug never changes, and a deleted slug is never given to another link.

Get started

One key in one header.

Every call goes to https://api.qsy.no/v1. Send your key with each request:

HTTP
Authorization: Bearer qsy_live_…

You create keys under Account, API keys, once the account is on Business. The key is shown once. We only store a SHA-256 fingerprint and the first characters (qsy_live_ plus six more) so you can recognise it in the list. If you lose it, create a new one. A key can expire after 30, 90 or 365 days.

Go to API keys

Access per key

You choose what each key can do. New keys start with only read access ticked.

Access per key
ScopeGrants
links:readRead links and folders, and fetch a link's QR code.
links:writeCreate, update and delete links and folders.
styles:readRead saved styles.
analytics:readRead scan statistics.
qr:writeGenerate static QR codes (POST /v1/qr).

Quickstart

From key to printed QR code in four steps.

The examples use curl and read the key from the QSY_API_KEY environment variable.

  1. 01

    Create a key

    Under Account, API keys. Tick links:write and analytics:read, and optionally pick a folder to lock the key to. Put the key in your environment:

    shell
    export QSY_API_KEY="qsy_live_…"
  2. 02

    Create a link with your own id

    external_id ties the code to the table in your system. The Idempotency-Key makes sure a retry after a dropped connection does not create a duplicate. The response includes id, short_url and qr_encoded_url.

    curl
    curl -X POST "https://api.qsy.no/v1/links" \
      -H "Authorization: Bearer $QSY_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: 0e6c4b8a-2f1d-4d7e-9b3a-5c8f1e2d4a6b" \
      -d '{
      "destination_url": "https://bistro-nord.no/meny?utm_source=qr&utm_medium=table",
      "title": "Table 12 menu",
      "folder": "Bistro Nord/Tables",
      "external_id": "pos-table-12",
      "metadata": {
        "table": 12,
        "location": "grunerlokka"
      },
      "style_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d"
    }'
  3. 03

    Fetch the QR code as SVG

    Vector, ready for print. The code points to the short link, so you can change the destination later without reprinting.

    curl
    curl "https://api.qsy.no/v1/links/3f6c2a9e-8b1d-4c5e-9a7f-2d4b6e8c1a3f/qr" \
      -H "Authorization: Bearer $QSY_API_KEY" \
      -o qr.svg
  4. 04

    Read the scan counts

    Dates are calendar days in Europe/Oslo. If analytics are down, the API answers 503, never zeros.

    curl
    curl "https://api.qsy.no/v1/links/3f6c2a9e-8b1d-4c5e-9a7f-2d4b6e8c1a3f/stats?from=2026-10-19&to=2026-10-25" \
      -H "Authorization: Bearer $QSY_API_KEY"

Later, change the destination with PATCH /v1/links/{id}, or sync your whole table list with PUT /v1/links/by-external-id/{external_id}.

Concepts

What to know before you build.

Folders and folder-locked keys

Folders organise links: up to 50 per account, five levels deep. A link created without a folder goes into "Via API". With a path such as "Bistro Nord/Tables" each level is found or created, without duplicates. When the limit is reached you get 409 folder_limit_reached, never a silent placement somewhere else.

A key locked to a folder sees that folder as a top-level folder. Links elsewhere do not exist for it (404). New links go into the folder, and the key cannot move or delete its own folder. If you delete the folder, the key is revoked.

Upserts with external_id

external_id is your id: up to 128 characters and unique per account. PUT /v1/links/by-external-id/{external_id} creates the link (201) if it is missing, and updates only the fields you send (200) if it exists. The call is safe to repeat. metadata is a free-form JSON object of up to 4 KB that travels with the link.

curl
curl -X PUT "https://api.qsy.no/v1/links/by-external-id/pos-table-12" \
  -H "Authorization: Bearer $QSY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "destination_url": "https://bistro-nord.no/meny?utm_source=qr&utm_medium=table",
  "title": "Table 12 menu",
  "folder": "Bistro Nord/Tables"
}'

Idempotency-Key

Works on POST /v1/links and POST /v1/links/bulk. The same key with the same body within 24 hours returns the stored response, with the header Idempotent-Replayed: true. The same key with a different body returns 409 idempotency_key_reused. Use a new UUID per operation.

Styles and SVG

You make styles in the studio, and GET /v1/styles gives you their ids. Send style_id when you create a link and the style is copied into it. If you change the style later, existing links keep theirs. A qr_config sent directly wins over style_id. QR codes are delivered as SVG. If you need PNG, convert the SVG on your side.

Errors

Every error has the same shape: a stable machine code, an English message and a request_id. The status always belongs to the code. For validation errors, details lists every invalid field. Infrastructure problems return 500 or 503, never 401 or 403.

422 · application/json
{
  "error": {
    "code": "validation_error",
    "message": "The request contains invalid fields.",
    "details": [
      {
        "field": "external_id",
        "code": "too_big",
        "message": "external_id must be 1-128 characters."
      }
    ],
    "request_id": "req_5f2c9a0e7b1d4c3a8e6f0b2d"
  }
}
Error codes
CodeStatusMeaning
bad_request400Invalid JSON, a body that is not an object, an invalid Idempotency-Key, or nothing to update.
key_expired401The key has passed its expiry date. Create a new one.
unauthorized401The key is missing, malformed, unknown or revoked.
forbidden403The account has no API access, the key lacks the scope, or a folder-locked key tried to move or delete its own folder.
not_found404Does not exist, belongs to another account, or is outside the key's folder.
conflict409The request conflicts with what is stored.
duplicate_external_id409Another link on the account already uses this external_id.
folder_exists409A folder with the same name already exists there. details.id is that folder.
folder_limit_reached409The folder limit is reached. details has limit and used.
folder_not_empty409The folder has contents. Send ?force=true to move them up one level.
idempotency_key_in_use409A request with the same Idempotency-Key is still running.
idempotency_key_reused409The Idempotency-Key was used before with a different body.
link_limit_reached409The plan's limit of active links is reached. details has limit and used.
unsupported_media_type415Send the body with Content-Type: application/json.
validation_error422One or more fields are invalid. details lists them.
rate_limited429Too many requests. Wait as many seconds as Retry-After says.
internal_error500Our fault. Retry, and send us the request_id if it persists.
service_unavailable503A service we depend on is temporarily down. Retry after Retry-After.

Rate limits

600 requests per minute per key and 1200 per account, in a sliding window. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (Unix time in seconds). On 429, wait as many seconds as Retry-After says.

HTTP
X-Request-Id: req_5f2c9a0e7b1d4c3a8e6f0b2d
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 599
X-RateLimit-Reset: 1793008860

When a change is live

Short links are served from the edge, close to whoever scans. Write calls answer with edge_synced: true when the change is live immediately, and false when it will be live within seconds. A brand-new link may then answer "not found" for a brief moment.

Time zone, pagination and CORS

Statistics use calendar days in Europe/Oslo. Timestamps are ISO 8601 in UTC. Lists are paginated with next_cursor until it is null. The API has no CORS. It is built for server-to-server use, so a key must never live in a browser or app.

Limits

The numbers, in one place.

The number of active links follows your plan and is listed on the pricing page. Everything else applies to every account with API access.

The numbers, in one place.
LimitValue
Requests per minute per key600
Requests per minute per account1,200
Active keys per account20
Links per page, default50
Links per page, maximum100
Operations per bulk call100
QR size, smallest (px)128
QR size, largest (px)2,048
QR size, default (px)1,024
Characters in a static QR code2,953
Characters in title200
Characters in destination_url4,096
Characters in external_id128
Bytes in metadata (compact JSON)4,096
Folders per account50
Levels in the folder tree5
Characters in a folder name60
Hours an Idempotency-Key is remembered24
Characters in an Idempotency-Key255
Days in the statistics window, default30
Countries in the statistics (top)5

Privacy

Numbers, not people.

Statistics count scans per day, device, source, country and time of day. We never store IP addresses. A country is only shown when there are enough scans that nobody can be singled out, and the API returns no cities. The data stays in the EU.

Reference

Every endpoint.

This reference is generated from the OpenAPI specification, the same file our tests keep in step with the code.

Download the specification (OpenAPI 3.1, JSON)

Links

get/v1/links/by-external-id/{external_id}Get a link by external_id

The link with this external_id. A link outside the key's folder → 404.

Scope links:read

Parameters

GET /links/by-external-id/{external_id} · Parameters
NameInTypeRequiredDescription
external_idpathstringyesYour identifier, percent-encoded. Avoid "/" in external ids.
expandquery"qr_config"noComma-separated. Only qr_config is supported; unknown values → 422.
Example · curl
curl "https://api.qsy.no/v1/links/by-external-id/pos-table-12" \
  -H "Authorization: Bearer $QSY_API_KEY"
Response · 200 · application/json
{
  "id": "3f6c2a9e-8b1d-4c5e-9a7f-2d4b6e8c1a3f",
  "slug": "k7qm2x",
  "short_url": "https://qsy.no/k7qm2x",
  "qr_encoded_url": "HTTPS://QSY.NO/K7QM2X",
  "destination_url": "https://bistro-nord.no/meny?utm_source=qr&utm_medium=table",
  "title": "Table 12 menu",
  "active": true,
  "use_shortlink": true,
  "folder_id": "0b2d4f6a-8c1e-4a3b-9d5f-7e9a1c3b5d7f",
  "folder_path": "Bistro Nord/Tables",
  "external_id": "pos-table-12",
  "metadata": {
    "table": 12,
    "location": "grunerlokka"
  },
  "created_at": "2026-10-25T10:00:00.000Z",
  "updated_at": "2026-10-25T10:00:00.000Z",
  "edge_synced": null
}
put/v1/links/by-external-id/{external_id}Create or update a link by external_id (upsert)

Missing → created with the POST /links rules (destination_url required) → 201. Exists → updated with the PATCH rules (only the fields you send change; an empty object changes nothing) → 200. external_id in the body is optional but must match the path. Naturally idempotent, so no Idempotency-Key is needed. If the id is used by a link outside a folder-scoped key's folder → 409 duplicate_external_id.

Scope links:write

Parameters

PUT /links/by-external-id/{external_id} · Parameters
NameInTypeRequiredDescription
external_idpathstringyesYour identifier, percent-encoded. Avoid "/" in external ids.

Body · UpdateLinkRequest

PUT /links/by-external-id/{external_id} · Body
NameTypeRequiredDescription
destination_urlstringnohttp(s) URL, or a mailto:, tel:, sms:, WIFI: or vCard destination. UTM goes straight into the URL and is stored as sent. On redirect, qsy.no rewrites utm_medium=qr (case-insensitive) to utm_medium=qsy.no-shortlink when the shared short link is opened (lowercase path, no ?src=qr); QR scans keep qr, other media and parameters are never changed, and utm_medium is never added.
titlestring | nullnoHuman-readable name. Trimmed; empty → null.
activebooleannoInactive links stop redirecting. Only active links count towards the plan limit.
use_shortlinkbooleannotrue (default): the QR code encodes the short link, so the destination can be changed after printing. false: the QR code encodes destination_url directly (no redirect, no scan statistics). WIFI: and vCard destinations are always encoded directly.
qr_configQrConfig | nullnoFull QR style. Wins over style_id. Stored on the link (replaced as a whole on update).
style_idstring · uuid | nullnoCopy a saved style from your account into the link. Input only (copy model): later edits to the style do not change existing links, and style_id is never returned.
folder_idstring · uuid | nullnoTarget folder. Must be visible to the key. On update, null moves the link to the account root (account-wide keys only).
folderstring | nullnoFolder path such as "Bistro Nord/Tables". Each level is found (case-insensitive) or created, relative to the key's root. Max 5 levels, each 1-60 characters. Send folder_id or folder, not both.
external_idstring | nullnoYour own identifier (POS table id, product id …). Unique per account. On update, null clears it.
metadataobjectnoFree-form JSON object, at most 4096 bytes as compact JSON. Replaced as a whole on update (send {} to clear).
Example · curl
curl -X PUT "https://api.qsy.no/v1/links/by-external-id/pos-table-12" \
  -H "Authorization: Bearer $QSY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "destination_url": "https://bistro-nord.no/meny?utm_source=qr&utm_medium=table",
  "title": "Table 12 menu",
  "folder": "Bistro Nord/Tables"
}'
Response · 200 · application/json
{
  "id": "3f6c2a9e-8b1d-4c5e-9a7f-2d4b6e8c1a3f",
  "slug": "k7qm2x",
  "short_url": "https://qsy.no/k7qm2x",
  "qr_encoded_url": "HTTPS://QSY.NO/K7QM2X",
  "destination_url": "https://bistro-nord.no/meny?utm_source=qr&utm_medium=table",
  "title": "Table 12 menu",
  "active": true,
  "use_shortlink": true,
  "folder_id": "0b2d4f6a-8c1e-4a3b-9d5f-7e9a1c3b5d7f",
  "folder_path": "Bistro Nord/Tables",
  "external_id": "pos-table-12",
  "metadata": {
    "table": 12,
    "location": "grunerlokka"
  },
  "created_at": "2026-10-25T10:00:00.000Z",
  "updated_at": "2026-10-25T10:00:00.000Z",
  "edge_synced": true
}

Folders

get/v1/foldersList folders

All folders visible to the key as a flat list sorted by path. A folder-scoped key sees its folder as a top-level folder (parent_id null).

Scope links:read

Example · curl
curl "https://api.qsy.no/v1/folders" \
  -H "Authorization: Bearer $QSY_API_KEY"
Response · 200 · application/json
{
  "folders": [
    {
      "id": "7a1e4c2b-5d3f-4e6a-8b9c-0d1e2f3a4b5c",
      "name": "Bistro Nord",
      "parent_id": null,
      "path": "Bistro Nord",
      "link_count": 2,
      "subtree_link_count": 14,
      "created_at": "2026-10-25T10:00:00.000Z",
      "updated_at": "2026-10-25T10:00:00.000Z"
    },
    {
      "id": "0b2d4f6a-8c1e-4a3b-9d5f-7e9a1c3b5d7f",
      "name": "Tables",
      "parent_id": "7a1e4c2b-5d3f-4e6a-8b9c-0d1e2f3a4b5c",
      "path": "Bistro Nord/Tables",
      "link_count": 12,
      "subtree_link_count": 12,
      "created_at": "2026-10-25T10:00:00.000Z",
      "updated_at": "2026-10-25T10:00:00.000Z"
    }
  ]
}
post/v1/foldersCreate a folder

Max 50 folders per account, nested at most 5 levels. A sibling with the same name (case-insensitive) → 409 folder_exists with its id in details.

Scope links:write

Body · CreateFolderRequest

POST /folders · Body
NameTypeRequiredDescription
namestringyesTrimmed. Must not contain "/". Unique among siblings (case-insensitive).
parent_idstring · uuid | nullnoOmitted or null → the key's root.
Example · curl
curl -X POST "https://api.qsy.no/v1/folders" \
  -H "Authorization: Bearer $QSY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Bistro Nord"
}'
Response · 201 · application/json
{
  "id": "7a1e4c2b-5d3f-4e6a-8b9c-0d1e2f3a4b5c",
  "name": "Bistro Nord",
  "parent_id": null,
  "path": "Bistro Nord",
  "link_count": 0,
  "subtree_link_count": 0,
  "created_at": "2026-10-25T10:00:00.000Z",
  "updated_at": "2026-10-25T10:00:00.000Z"
}
get/v1/folders/{id}Get a folder

One folder with link counts.

Scope links:read

Parameters

GET /folders/{id} · Parameters
NameInTypeRequiredDescription
idpathstring · uuidyesFolder id. A value that is not a UUID → 404.
Example · curl
curl "https://api.qsy.no/v1/folders/7a1e4c2b-5d3f-4e6a-8b9c-0d1e2f3a4b5c" \
  -H "Authorization: Bearer $QSY_API_KEY"
Response · 200 · application/json
{
  "id": "7a1e4c2b-5d3f-4e6a-8b9c-0d1e2f3a4b5c",
  "name": "Bistro Nord",
  "parent_id": null,
  "path": "Bistro Nord",
  "link_count": 2,
  "subtree_link_count": 14,
  "created_at": "2026-10-25T10:00:00.000Z",
  "updated_at": "2026-10-25T10:00:00.000Z"
}
patch/v1/folders/{id}Rename or move a folder

Rename and/or move. A folder-scoped key can rename its own folder but not move it (403).

Scope links:write

Parameters

PATCH /folders/{id} · Parameters
NameInTypeRequiredDescription
idpathstring · uuidyesFolder id. A value that is not a UUID → 404.

Body · UpdateFolderRequest

PATCH /folders/{id} · Body
NameTypeRequiredDescription
namestringno
parent_idstring · uuid | nullnoMove the folder. null → the key's root. Cannot move into itself or a subfolder.
Example · curl
curl -X PATCH "https://api.qsy.no/v1/folders/7a1e4c2b-5d3f-4e6a-8b9c-0d1e2f3a4b5c" \
  -H "Authorization: Bearer $QSY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Bistro Nord Grünerløkka"
}'
Response · 200 · application/json
{
  "id": "7a1e4c2b-5d3f-4e6a-8b9c-0d1e2f3a4b5c",
  "name": "Bistro Nord Grünerløkka",
  "parent_id": null,
  "path": "Bistro Nord Grünerløkka",
  "link_count": 2,
  "subtree_link_count": 14,
  "created_at": "2026-10-25T10:00:00.000Z",
  "updated_at": "2026-10-25T10:00:00.000Z"
}
delete/v1/folders/{id}Delete a folder

An empty folder is deleted. A folder with subfolders or links → 409 folder_not_empty with counts, unless ?force=true: then its contents move up one level and no link is ever deleted. API keys limited to the deleted folder are revoked. A folder-scoped key cannot delete its own folder (403).

Scope links:write

Parameters

DELETE /folders/{id} · Parameters
NameInTypeRequiredDescription
idpathstring · uuidyesFolder id. A value that is not a UUID → 404.
forcequery"true" | "false"no"true" moves contents up one level instead of refusing.
Example · curl
curl -X DELETE "https://api.qsy.no/v1/folders/7a1e4c2b-5d3f-4e6a-8b9c-0d1e2f3a4b5c" \
  -H "Authorization: Bearer $QSY_API_KEY"
Response · 200 · application/json
{
  "id": "7a1e4c2b-5d3f-4e6a-8b9c-0d1e2f3a4b5c",
  "deleted": true,
  "contents_moved_to": null
}

QR codes

get/v1/links/{id}/qrGet the QR code of a link (SVG)

Renders the link's QR code as SVG. It encodes qr_encoded_url: the short link, or the destination only for use_shortlink=false and WIFI:/vCard links. No query parameter can change what the code points to. Style overrides apply to this rendering only; nothing is stored.

Scope links:read

Parameters

GET /links/{id}/qr · Parameters
NameInTypeRequiredDescription
idpathstring · uuidyesLink id. A value that is not a UUID → 404.
formatquery"svg"noOnly svg.
sizequeryintegernoPixel size of the SVG coordinate system, clamped to 128-2048 (default 1024; invalid → 1024).
style_idquerystring · uuidnoRender with a saved style instead of the link's own.
qr_configquerystringnoURL-encoded JSON QrConfig, full or partial (merged over the defaults). Wins over style_id.
Example · curl
curl "https://api.qsy.no/v1/links/3f6c2a9e-8b1d-4c5e-9a7f-2d4b6e8c1a3f/qr" \
  -H "Authorization: Bearer $QSY_API_KEY" \
  -o qr.svg
Response · 200 · image/svg+xml
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">…</svg>
post/v1/qrGenerate a static QR code (SVG)

Encodes data literally: no short link, no tracking, nothing stored. For labels, WiFi and contact cards that must work without a redirect. The code can never be changed after printing.

Scope qr:write

Body · StaticQrRequest

POST /qr · Body
NameTypeRequiredDescription
datastringyesEncoded literally. You build the WIFI:, BEGIN:VCARD or geo: string yourself.
type"url" | "wifi" | "vcard" | "text" | "geo"yesDocuments the content; it does not change what is encoded.
qr_configQrConfignoFull style. Wins over style_id.
style_idstring · uuidnoA saved style on your account.
format"svg"noOnly SVG. Rasterise it yourself if you need PNG.
sizeintegernoOut-of-range values are clamped, invalid values fall back to 1024.
Example · curl
curl -X POST "https://api.qsy.no/v1/qr" \
  -H "Authorization: Bearer $QSY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "data": "WIFI:T:WPA;S:BistroNord-Guest;P:velkommen2026;;",
  "type": "wifi",
  "style_id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
  "size": 1024
}' \
  -o qr.svg
Response · 200 · image/svg+xml
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">…</svg>

Statistics

get/v1/links/{id}/statsGet scan statistics for a link

Aggregated scans for one link. Dates are calendar days in Europe/Oslo. Counters only: no IP addresses, no cities, no personal data. If analytics are unavailable the API answers 503 (never zeros).

Scope analytics:read

Parameters

GET /links/{id}/stats · Parameters
NameInTypeRequiredDescription
idpathstring · uuidyesLink id. A value that is not a UUID → 404.
fromquerystring · datenoYYYY-MM-DD (Europe/Oslo). Default: 29 days before today (a 30-day window).
toquerystring · datenoYYYY-MM-DD (Europe/Oslo), inclusive. Default: today. Must not be before from, and the window can be at most 1096 days.
Example · curl
curl "https://api.qsy.no/v1/links/3f6c2a9e-8b1d-4c5e-9a7f-2d4b6e8c1a3f/stats?from=2026-10-19&to=2026-10-25" \
  -H "Authorization: Bearer $QSY_API_KEY"
Response · 200 · application/json
{
  "link_id": "3f6c2a9e-8b1d-4c5e-9a7f-2d4b6e8c1a3f",
  "from": "2026-10-19",
  "to": "2026-10-25",
  "granularity": "day",
  "total": 412,
  "all_time": 3871,
  "series": [
    {
      "bucket": "2026-10-19",
      "count": 48
    },
    {
      "bucket": "2026-10-20",
      "count": 51
    },
    {
      "bucket": "2026-10-21",
      "count": 44
    },
    {
      "bucket": "2026-10-22",
      "count": 57
    },
    {
      "bucket": "2026-10-23",
      "count": 79
    },
    {
      "bucket": "2026-10-24",
      "count": 91
    },
    {
      "bucket": "2026-10-25",
      "count": 42
    }
  ],
  "breakdowns": {
    "device": {
      "mobile": 398,
      "desktop": 6,
      "tablet": 8,
      "unknown": 0
    },
    "source": {
      "qr": 401,
      "link": 11,
      "unknown": 0
    },
    "country": [
      {
        "country": "NO",
        "count": 371
      },
      {
        "country": "SE",
        "count": 22
      },
      {
        "country": "DE",
        "count": 19
      }
    ],
    "daypart": {
      "night": 9,
      "morning": 61,
      "day": 158,
      "evening": 184
    }
  }
}

Styles

get/v1/stylesList styles

Saved QR styles on the account, newest first. Styles are made in the qsy studio; the API only reads them. Account-level: not limited by folder scope.

Scope styles:read

Example · curl
curl "https://api.qsy.no/v1/styles" \
  -H "Authorization: Bearer $QSY_API_KEY"
Response · 200 · application/json
{
  "styles": [
    {
      "id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "name": "Bistro Nord",
      "qr_config": {
        "mode": "styled",
        "shape": "square",
        "errorCorrectionLevel": "M",
        "margin": 0,
        "dots": {
          "type": "extra-rounded",
          "color": "#1A1A1A",
          "gradient": null
        },
        "cornersSquare": {
          "type": "extra-rounded",
          "color": "#1A1A1A",
          "gradient": null
        },
        "cornersDot": {
          "type": "dot",
          "color": "#C0392B",
          "gradient": null
        },
        "background": {
          "color": "#FAFAF7",
          "transparent": false,
          "gradient": null,
          "round": 0
        },
        "image": null,
        "imageOptions": {
          "imageSize": 0.4,
          "margin": 2,
          "mode": "center"
        }
      },
      "created_at": "2026-10-25T10:00:00.000Z"
    }
  ]
}
get/v1/styles/{id}Get a style

One saved style.

Scope styles:read

Parameters

GET /styles/{id} · Parameters
NameInTypeRequiredDescription
idpathstring · uuidyesStyle id. A value that is not a UUID → 404.
Example · curl
curl "https://api.qsy.no/v1/styles/9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d" \
  -H "Authorization: Bearer $QSY_API_KEY"
Response · 200 · application/json
{
  "id": "9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
  "name": "Bistro Nord",
  "qr_config": {
    "mode": "styled",
    "shape": "square",
    "errorCorrectionLevel": "M",
    "margin": 0,
    "dots": {
      "type": "extra-rounded",
      "color": "#1A1A1A",
      "gradient": null
    },
    "cornersSquare": {
      "type": "extra-rounded",
      "color": "#1A1A1A",
      "gradient": null
    },
    "cornersDot": {
      "type": "dot",
      "color": "#C0392B",
      "gradient": null
    },
    "background": {
      "color": "#FAFAF7",
      "transparent": false,
      "gradient": null,
      "round": 0
    },
    "image": null,
    "imageOptions": {
      "imageSize": 0.4,
      "margin": 2,
      "mode": "center"
    }
  },
  "created_at": "2026-10-25T10:00:00.000Z",
  "updated_at": "2026-10-25T10:05:00.000Z"
}
get/v1/styles/{id}/previewPreview a style (SVG)

Renders a saved style as an SVG preview for style pickers. The code in the image always encodes the fixed sample URL https://qsy.no; no parameter can change what it encodes. Use id "default" for the house default style. To get the code of a real link, use GET /links/{id}/qr.

Scope styles:read

Parameters

GET /styles/{id}/preview · Parameters
NameInTypeRequiredDescription
idpathstringyesStyle id (UUID), or "default".
formatquery"svg"noOnly svg.
sizequeryintegernoPixel size of the SVG coordinate system, clamped to 128-1024 (default 512; invalid → 512).
Example · curl
curl "https://api.qsy.no/v1/styles/id/preview" \
  -H "Authorization: Bearer $QSY_API_KEY" \
  -o qr.svg
Response · 200 · image/svg+xml
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">…</svg>

The API comes with Business.

With Business you create keys under Account and can start right away. Everything else Business includes is on the pricing page.