API for utviklere
Koble qsy til systemene du allerede har.
Et REST-API som snakker JSON. Kassesystemet, bookingløsningen eller nettbutikken lager kortlenker med QR-kode, bytter hvor de peker og henter skann-tall. API-et følger med Bedrift.
For integratorer
Laget for systemer som eier mange koder.
Tenk deg et restaurantsystem med én QR-kode per bord på tjue restauranter. Slik ser det ut med qsy:
- Én mappe og én nøkkel per restaurant. En nøkkel kan låses til én mappe. Den ser bare sine egne koder, og en feil i én integrasjon når aldri de andre.
- Dine id-er, ikke våre. Gi hver kode din egen external_id, for eksempel bordnummeret i kassen. Ett PUT-kall lager koden om den mangler og oppdaterer den om den finnes.
- Trygt å prøve igjen. Send en Idempotency-Key når du oppretter. Faller nettet midt i kallet, gir et nytt forsøk samme svar i stedet for en kopi.
- Koden på trykk står stille. Du bytter destinasjon så ofte du vil. Slugen endres aldri, og en slettet slug gis aldri til en annen lenke.
Kom i gang
En nøkkel i en header.
Alle kall går til https://api.qsy.no/v1. Send nøkkelen i hver forespørsel:
Authorization: Bearer qsy_live_…Nøkler lager du under Konto, API-nøkler, når kontoen har Bedrift. Du ser nøkkelen én gang. Vi lagrer bare et SHA-256-avtrykk og de første tegnene (qsy_live_ og seks til), så du kjenner den igjen i lista. Mister du den, lager du en ny. En nøkkel kan få utløpsdato etter 30, 90 eller 365 dager.
Tilgang per nøkkel
Du krysser av hva hver nøkkel kan gjøre. Nye nøkler har bare lesetilgang avkrysset fra start.
| Scope | Gir tilgang til |
|---|---|
links:read | Lese lenker og mapper, og hente QR-koden til en lenke. |
links:write | Lage, endre og slette lenker og mapper. |
styles:read | Lese lagrede stiler. |
analytics:read | Lese skann-statistikk. |
qr:write | Lage statiske QR-koder (POST /v1/qr). |
Hurtigstart
Fra nøkkel til QR-kode på trykk, i fire steg.
Eksemplene bruker curl og leser nøkkelen fra miljøvariabelen QSY_API_KEY.
- 01
Lag en nøkkel
Under Konto, API-nøkler. Kryss av links:write og analytics:read, og velg eventuelt en mappe nøkkelen skal låses til. Legg nøkkelen i miljøet ditt:
shell export QSY_API_KEY="qsy_live_…" - 02
Lag en lenke med din egen id
external_id knytter koden til bordet i systemet ditt. Idempotency-Key gjør at et nytt forsøk etter et brudd ikke lager en kopi. Svaret har id, short_url og 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" }' - 03
Hent QR-koden som SVG
Vektor, klar for trykk. Koden peker på kortlenka, så du kan bytte destinasjon senere uten å trykke på nytt.
curl curl "https://api.qsy.no/v1/links/3f6c2a9e-8b1d-4c5e-9a7f-2d4b6e8c1a3f/qr" \ -H "Authorization: Bearer $QSY_API_KEY" \ -o qr.svg - 04
Les skann-tallene
Datoene er kalenderdager i Europe/Oslo. Er analysen nede, svarer API-et 503, aldri med nuller.
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"
Senere bytter du destinasjon med PATCH /v1/links/{id}, eller synker hele bordlista med PUT /v1/links/by-external-id/{external_id}.
Begreper
Det du trenger å vite før du bygger.
Mapper og mappe-låste nøkler
Mapper ordner lenkene, opptil 50 per konto og fem nivåer dypt. Lager du en lenke uten mappe, havner den i «Via API». Med en sti som «Bistro Nord/Bord» finnes eller lages hvert nivå, uten duplikater. Er taket nådd, får du 409 folder_limit_reached, aldri en stille plassering et annet sted.
En nøkkel som er låst til en mappe, ser mappa som en toppmappe. Lenker andre steder finnes ikke for den (404). Nye lenker havner i mappa, og nøkkelen kan ikke flytte eller slette sin egen mappe. Sletter du mappa, trekkes nøkkelen tilbake.
Upsert med external_id
external_id er din id, opptil 128 tegn og unik per konto. PUT /v1/links/by-external-id/{external_id} lager lenka (201) om den mangler, og oppdaterer bare feltene du sender (200) om den finnes. Kallet tåler å bli gjentatt. metadata er et fritt JSON-objekt på opptil 4 KB som følger lenka.
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
Gjelder POST /v1/links og POST /v1/links/bulk. Samme nøkkel og samme body innen 24 timer gir det lagrede svaret tilbake, med headeren Idempotent-Replayed: true. Samme nøkkel med en annen body gir 409 idempotency_key_reused. Bruk en ny UUID per operasjon.
Stiler og SVG
Stiler lager du i studioet, og GET /v1/styles gir deg id-ene. Sender du style_id når du lager en lenke, kopieres stilen inn i lenka. Endrer du stilen senere, beholder eksisterende lenker sin. qr_config sendt direkte vinner over style_id. QR-kodene leveres som SVG. Trenger du PNG, lager du det fra SVG-en.
Feil
Hver feil har samme form: en fast maskin-kode, en melding på engelsk og en request_id. Statusen hører alltid til koden. Ved valideringsfeil lister details hvert felt som er feil. Driftsproblemer gir 500 eller 503, aldri 401 eller 403.
{
"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"
}
}| Kode | Status | Betyr |
|---|---|---|
bad_request | 400 | Ugyldig JSON, en body som ikke er et objekt, en ugyldig Idempotency-Key eller ingenting å endre. |
key_expired | 401 | Nøkkelen har passert utløpsdatoen. Lag en ny. |
401 | Nøkkelen mangler, har feil form, er ukjent eller er trukket tilbake. | |
forbidden | 403 | Kontoen har ikke API-tilgang, nøkkelen mangler scope, eller en mappe-låst nøkkel prøver å flytte eller slette sin egen mappe. |
not_found | 404 | Finnes ikke, tilhører en annen konto eller ligger utenfor nøkkelens mappe. |
conflict | 409 | Forespørselen kolliderer med det som er lagret. |
duplicate_external_id | 409 | En annen lenke på kontoen bruker denne external_id. |
folder_exists | 409 | En mappe med samme navn finnes der fra før. details.id er den mappa. |
folder_limit_reached | 409 | Taket på antall mapper er nådd. details har limit og used. |
folder_not_empty | 409 | Mappa har innhold. Send ?force=true for å flytte innholdet ett nivå opp. |
idempotency_key_in_use | 409 | Et kall med samme Idempotency-Key pågår fortsatt. |
idempotency_key_reused | 409 | Idempotency-Key er brukt før med en annen body. |
link_limit_reached | 409 | Planens tak for aktive lenker er nådd. details har limit og used. |
unsupported_media_type | 415 | Bodyen må sendes med Content-Type: application/json. |
validation_error | 422 | Ett eller flere felt er ugyldige. details lister dem. |
rate_limited | 429 | For mange kall. Vent så mange sekunder som Retry-After sier. |
internal_error | 500 | Feil hos oss. Prøv igjen, og send oss request_id om det varer. |
503 | En tjeneste vi bruker er midlertidig nede. Prøv igjen etter Retry-After. |
Grenser for antall kall
600 kall i minuttet per nøkkel og 1200 per konto, i et glidende vindu. Hvert svar har X-RateLimit-Limit, X-RateLimit-Remaining og X-RateLimit-Reset (Unix-tid i sekunder). Ved 429 venter du så mange sekunder som Retry-After sier.
X-Request-Id: req_5f2c9a0e7b1d4c3a8e6f0b2d
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 599
X-RateLimit-Reset: 1793008860Når en endring er ute
Kortlenkene serveres fra kanten, nær den som skanner. Skrivekall svarer med edge_synced: true når endringen er ute med en gang, og false når den kommer i løpet av sekunder. En helt ny lenke kan da svare «finnes ikke» et lite øyeblikk.
Tidssone, paginering og CORS
Statistikk regnes i kalenderdager i Europe/Oslo. Tidsstempler er ISO 8601 i UTC. Lister pagineres med next_cursor til den er null. API-et har ingen CORS. Det er laget for server til server, så nøkkelen skal aldri ligge i en nettleser eller app.
Grenser
Tallene, samlet ett sted.
Antall aktive lenker følger planen og står på prissiden. Resten gjelder alle kontoer med API-tilgang.
| Grense | Verdi |
|---|---|
| Kall per minutt per nøkkel | 600 |
| Kall per minutt per konto | 1 200 |
| Aktive nøkler per konto | 20 |
| Lenker per side, standard | 50 |
| Lenker per side, maks | 100 |
| Operasjoner per bulk-kall | 100 |
| QR-størrelse, minst (px) | 128 |
| QR-størrelse, størst (px) | 2 048 |
| QR-størrelse, standard (px) | 1 024 |
| Tegn i en statisk QR-kode | 2 953 |
| Tegn i title | 200 |
| Tegn i destination_url | 4 096 |
| Tegn i external_id | 128 |
| Byte i metadata (kompakt JSON) | 4 096 |
| Mapper per konto | 50 |
| Nivåer i mappetreet | 5 |
| Tegn i et mappenavn | 60 |
| Timer en Idempotency-Key huskes | 24 |
| Tegn i en Idempotency-Key | 255 |
| Dager i statistikk-vinduet, standard | 30 |
| Land i statistikken (topp) | 5 |
Personvern
Tall, ikke personer.
Statistikken teller skann per dag, enhet, kilde, land og tid på døgnet. Vi lagrer aldri IP-adresser. Land vises bare når det er nok skann til at ingen kan pekes ut, og API-et gir ingen byer. Dataene ligger i EU.
Referanse
Alle endepunkter.
Referansen lages fra OpenAPI-spesifikasjonen, den samme fila som testene holder lik koden. Beskrivelsene er på engelsk, som API-et.
Last ned spesifikasjonen (OpenAPI 3.1, JSON)
Lenker
get/v1/linksList lenker
Links visible to the key, newest first (created_at desc, id desc), keyset-paginated. Folder-scoped keys only see links in their folder subtree.
Scope links:read
Parametre
| Navn | I | Type | Påkrevd | Beskrivelse |
|---|---|---|---|---|
limit | spørring | integer | nei | Page size, 1-100 (default 50). Larger values are clamped to 100; 0, negative or non-numeric → 422. |
cursor | spørring | string | nei | next_cursor from the previous page. A tampered or invalid cursor → 422. |
folder_id | spørring | string | nei | Only links directly in this folder (not subfolders). "null" or empty → root links only. |
external_id | spørring | string | nei | Only the link with this external_id. |
expand | spørring | "qr_config" | nei | Comma-separated. Only qr_config is supported; unknown values → 422. |
curl "https://api.qsy.no/v1/links" \
-H "Authorization: Bearer $QSY_API_KEY"{
"links": [
{
"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
}
],
"next_cursor": "MjAyNi0xMC0yNVQxMDowMDowMC4wMDBafDNmNmMyYTll…"
}post/v1/linksOpprett lenke
Creates a short link with a QR code. Placement: folder_id, else folder (path, created as needed), else the key's scope folder (folder-scoped keys) or "Via API" (account-wide keys). Counts towards the active-link limit of the plan (atomic). Send an Idempotency-Key so a retry never creates a duplicate.
Scope links:write
Parametre
| Navn | I | Type | Påkrevd | Beskrivelse |
|---|---|---|---|---|
Idempotency-Key | header | string | nei | 1-255 visible ASCII characters (use a UUID per logical operation). Same key + same body within 24 h → the stored response is replayed with Idempotent-Replayed: true. Same key + different body → 409 idempotency_key_reused; still running → 409 idempotency_key_in_use. Keys are scoped to the API key. |
expand | spørring | "qr_config" | nei | Comma-separated. Only qr_config is supported; unknown values → 422. |
Body · CreateLinkRequest
| Navn | Type | Påkrevd | Beskrivelse |
|---|---|---|---|
destination_url | string | ja | http(s) URL (a bare domain gets https://), or a mailto:, tel:, sms:, WIFI: or vCard destination. Checked against Google Safe Browsing. 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. |
title | string | null | nei | Human-readable name. Trimmed; empty → null. |
active | boolean | nei | Inactive links stop redirecting. Only active links count towards the plan limit. |
use_shortlink | boolean | nei | true (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_config | QrConfig | null | nei | Full QR style. Wins over style_id. Stored on the link (replaced as a whole on update). |
style_id | string · uuid | null | nei | Copy 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_id | string · uuid | null | nei | Target folder. Must be visible to the key. On update, null moves the link to the account root (account-wide keys only). |
folder | string | null | nei | Folder 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_id | string | null | nei | Your own identifier (POS table id, product id …). Unique per account. On update, null clears it. |
metadata | object | nei | Free-form JSON object, at most 4096 bytes as compact JSON. Replaced as a whole on update (send {} to clear). |
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"
}'{
"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
}post/v1/links/bulkOpprett eller oppdater lenker i bulk
Up to 100 operations, run in order with exactly the same rules as POST /links and PATCH /links/{id}. Partial success is normal: the response is 200 and each result has ok true/false. If the request runs short of time, the remaining operations are not started and come back with error.code service_unavailable and details.reason "not_processed"; send those again (with a new Idempotency-Key). Supports Idempotency-Key.
Scope links:write
Parametre
| Navn | I | Type | Påkrevd | Beskrivelse |
|---|---|---|---|---|
Idempotency-Key | header | string | nei | 1-255 visible ASCII characters (use a UUID per logical operation). Same key + same body within 24 h → the stored response is replayed with Idempotent-Replayed: true. Same key + different body → 409 idempotency_key_reused; still running → 409 idempotency_key_in_use. Keys are scoped to the API key. |
expand | spørring | "qr_config" | nei | Comma-separated. Only qr_config is supported; unknown values → 422. |
Body · BulkRequest
| Navn | Type | Påkrevd | Beskrivelse |
|---|---|---|---|
operations | CreateLinkRequest | BulkUpdateItem[] | ja | Without id → create (same rules as POST /links). With id → update (same rules as PATCH /links/{id}). Run in order. |
curl -X POST "https://api.qsy.no/v1/links/bulk" \
-H "Authorization: Bearer $QSY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0e6c4b8a-2f1d-4d7e-9b3a-5c8f1e2d4a6b" \
-d '{
"operations": [
{
"destination_url": "https://bistro-nord.no/meny?t=13",
"external_id": "pos-table-13",
"folder": "Bistro Nord/Tables"
},
{
"id": "3f6c2a9e-8b1d-4c5e-9a7f-2d4b6e8c1a3f",
"destination_url": "https://bistro-nord.no/vinter-meny"
}
]
}'{
"results": [
{
"ok": true,
"index": 0,
"link": {
"id": "5d7f9b1c-3e5a-4c7e-9f1b-3d5f7a9c1e3b",
"slug": "p4rt8w",
"short_url": "https://qsy.no/p4rt8w",
"qr_encoded_url": "HTTPS://QSY.NO/P4RT8W",
"destination_url": "https://bistro-nord.no/meny?t=13",
"title": null,
"active": true,
"use_shortlink": true,
"folder_id": "0b2d4f6a-8c1e-4a3b-9d5f-7e9a1c3b5d7f",
"folder_path": "Bistro Nord/Tables",
"external_id": "pos-table-13",
"metadata": {},
"created_at": "2026-10-25T10:00:00.000Z",
"updated_at": "2026-10-25T10:00:00.000Z",
"edge_synced": true
}
},
{
"ok": false,
"index": 1,
"error": {
"code": "not_found",
"message": "No link found with this id."
}
}
],
"summary": {
"total": 2,
"succeeded": 1,
"failed": 1
}
}get/v1/links/by-external-id/{external_id}Hent lenke med external_id
The link with this external_id. A link outside the key's folder → 404.
Scope links:read
Parametre
| Navn | I | Type | Påkrevd | Beskrivelse |
|---|---|---|---|---|
external_id | sti | string | ja | Your identifier, percent-encoded. Avoid "/" in external ids. |
expand | spørring | "qr_config" | nei | Comma-separated. Only qr_config is supported; unknown values → 422. |
curl "https://api.qsy.no/v1/links/by-external-id/pos-table-12" \
-H "Authorization: Bearer $QSY_API_KEY"{
"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}Opprett eller oppdater med 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
Parametre
| Navn | I | Type | Påkrevd | Beskrivelse |
|---|---|---|---|---|
external_id | sti | string | ja | Your identifier, percent-encoded. Avoid "/" in external ids. |
Body · UpdateLinkRequest
| Navn | Type | Påkrevd | Beskrivelse |
|---|---|---|---|
destination_url | string | nei | http(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. |
title | string | null | nei | Human-readable name. Trimmed; empty → null. |
active | boolean | nei | Inactive links stop redirecting. Only active links count towards the plan limit. |
use_shortlink | boolean | nei | true (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_config | QrConfig | null | nei | Full QR style. Wins over style_id. Stored on the link (replaced as a whole on update). |
style_id | string · uuid | null | nei | Copy 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_id | string · uuid | null | nei | Target folder. Must be visible to the key. On update, null moves the link to the account root (account-wide keys only). |
folder | string | null | nei | Folder 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_id | string | null | nei | Your own identifier (POS table id, product id …). Unique per account. On update, null clears it. |
metadata | object | nei | Free-form JSON object, at most 4096 bytes as compact JSON. Replaced as a whole on update (send {} to clear). |
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"
}'{
"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
}get/v1/links/{id}Hent lenke
One link. Another account's link, or one outside the key's folder → 404.
Scope links:read
Parametre
| Navn | I | Type | Påkrevd | Beskrivelse |
|---|---|---|---|---|
id | sti | string · uuid | ja | Link id. A value that is not a UUID → 404. |
expand | spørring | "qr_config" | nei | Comma-separated. Only qr_config is supported; unknown values → 422. |
curl "https://api.qsy.no/v1/links/3f6c2a9e-8b1d-4c5e-9a7f-2d4b6e8c1a3f" \
-H "Authorization: Bearer $QSY_API_KEY"{
"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
}patch/v1/links/{id}Oppdater lenke
Change the destination (the printed QR code keeps working), title, active, use_shortlink, style, folder, external_id or metadata. The slug never changes. Turning use_shortlink on or off changes what the QR code encodes, so printed codes must be reprinted.
Scope links:write
Parametre
| Navn | I | Type | Påkrevd | Beskrivelse |
|---|---|---|---|---|
id | sti | string · uuid | ja | Link id. A value that is not a UUID → 404. |
expand | spørring | "qr_config" | nei | Comma-separated. Only qr_config is supported; unknown values → 422. |
Body · UpdateLinkRequest
| Navn | Type | Påkrevd | Beskrivelse |
|---|---|---|---|
destination_url | string | nei | http(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. |
title | string | null | nei | Human-readable name. Trimmed; empty → null. |
active | boolean | nei | Inactive links stop redirecting. Only active links count towards the plan limit. |
use_shortlink | boolean | nei | true (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_config | QrConfig | null | nei | Full QR style. Wins over style_id. Stored on the link (replaced as a whole on update). |
style_id | string · uuid | null | nei | Copy 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_id | string · uuid | null | nei | Target folder. Must be visible to the key. On update, null moves the link to the account root (account-wide keys only). |
folder | string | null | nei | Folder 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_id | string | null | nei | Your own identifier (POS table id, product id …). Unique per account. On update, null clears it. |
metadata | object | nei | Free-form JSON object, at most 4096 bytes as compact JSON. Replaced as a whole on update (send {} to clear). |
curl -X PATCH "https://api.qsy.no/v1/links/3f6c2a9e-8b1d-4c5e-9a7f-2d4b6e8c1a3f" \
-H "Authorization: Bearer $QSY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"destination_url": "https://bistro-nord.no/vinter-meny?utm_source=qr"
}'{
"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/vinter-meny?utm_source=qr",
"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:05:00.000Z",
"edge_synced": true
}delete/v1/links/{id}Slett lenke
Deletes the link and its scan data. The slug is retired for good: it will never be given to another link, so a printed code can never point somewhere unexpected. Use active=false instead if the code might come back.
Scope links:write
Parametre
| Navn | I | Type | Påkrevd | Beskrivelse |
|---|---|---|---|---|
id | sti | string · uuid | ja | Link id. A value that is not a UUID → 404. |
curl -X DELETE "https://api.qsy.no/v1/links/3f6c2a9e-8b1d-4c5e-9a7f-2d4b6e8c1a3f" \
-H "Authorization: Bearer $QSY_API_KEY"{
"id": "3f6c2a9e-8b1d-4c5e-9a7f-2d4b6e8c1a3f",
"deleted": true,
"edge_synced": true
}Mapper
get/v1/foldersList mapper
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
curl "https://api.qsy.no/v1/folders" \
-H "Authorization: Bearer $QSY_API_KEY"{
"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/foldersOpprett mappe
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
| Navn | Type | Påkrevd | Beskrivelse |
|---|---|---|---|
name | string | ja | Trimmed. Must not contain "/". Unique among siblings (case-insensitive). |
parent_id | string · uuid | null | nei | Omitted or null → the key's root. |
curl -X POST "https://api.qsy.no/v1/folders" \
-H "Authorization: Bearer $QSY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Bistro Nord"
}'{
"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}Hent mappe
One folder with link counts.
Scope links:read
Parametre
| Navn | I | Type | Påkrevd | Beskrivelse |
|---|---|---|---|---|
id | sti | string · uuid | ja | Folder id. A value that is not a UUID → 404. |
curl "https://api.qsy.no/v1/folders/7a1e4c2b-5d3f-4e6a-8b9c-0d1e2f3a4b5c" \
-H "Authorization: Bearer $QSY_API_KEY"{
"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}Gi nytt navn til eller flytt mappe
Rename and/or move. A folder-scoped key can rename its own folder but not move it (403).
Scope links:write
Parametre
| Navn | I | Type | Påkrevd | Beskrivelse |
|---|---|---|---|---|
id | sti | string · uuid | ja | Folder id. A value that is not a UUID → 404. |
Body · UpdateFolderRequest
| Navn | Type | Påkrevd | Beskrivelse |
|---|---|---|---|
name | string | nei | |
parent_id | string · uuid | null | nei | Move the folder. null → the key's root. Cannot move into itself or a subfolder. |
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"
}'{
"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}Slett mappe
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
Parametre
| Navn | I | Type | Påkrevd | Beskrivelse |
|---|---|---|---|---|
id | sti | string · uuid | ja | Folder id. A value that is not a UUID → 404. |
force | spørring | "true" | "false" | nei | "true" moves contents up one level instead of refusing. |
curl -X DELETE "https://api.qsy.no/v1/folders/7a1e4c2b-5d3f-4e6a-8b9c-0d1e2f3a4b5c" \
-H "Authorization: Bearer $QSY_API_KEY"{
"id": "7a1e4c2b-5d3f-4e6a-8b9c-0d1e2f3a4b5c",
"deleted": true,
"contents_moved_to": null
}QR-koder
get/v1/links/{id}/qrHent QR-koden til en lenke (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
Parametre
| Navn | I | Type | Påkrevd | Beskrivelse |
|---|---|---|---|---|
id | sti | string · uuid | ja | Link id. A value that is not a UUID → 404. |
format | spørring | "svg" | nei | Only svg. |
size | spørring | integer | nei | Pixel size of the SVG coordinate system, clamped to 128-2048 (default 1024; invalid → 1024). |
style_id | spørring | string · uuid | nei | Render with a saved style instead of the link's own. |
qr_config | spørring | string | nei | URL-encoded JSON QrConfig, full or partial (merged over the defaults). Wins over style_id. |
curl "https://api.qsy.no/v1/links/3f6c2a9e-8b1d-4c5e-9a7f-2d4b6e8c1a3f/qr" \
-H "Authorization: Bearer $QSY_API_KEY" \
-o qr.svg<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">…</svg>post/v1/qrLag statisk QR-kode (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
| Navn | Type | Påkrevd | Beskrivelse |
|---|---|---|---|
data | string | ja | Encoded literally. You build the WIFI:, BEGIN:VCARD or geo: string yourself. |
type | "url" | "wifi" | "vcard" | "text" | "geo" | ja | Documents the content; it does not change what is encoded. |
qr_config | QrConfig | nei | Full style. Wins over style_id. |
style_id | string · uuid | nei | A saved style on your account. |
format | "svg" | nei | Only SVG. Rasterise it yourself if you need PNG. |
size | integer | nei | Out-of-range values are clamped, invalid values fall back to 1024. |
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<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">…</svg>Statistikk
get/v1/links/{id}/statsHent skann-statistikk for en lenke
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
Parametre
| Navn | I | Type | Påkrevd | Beskrivelse |
|---|---|---|---|---|
id | sti | string · uuid | ja | Link id. A value that is not a UUID → 404. |
from | spørring | string · date | nei | YYYY-MM-DD (Europe/Oslo). Default: 29 days before today (a 30-day window). |
to | spørring | string · date | nei | YYYY-MM-DD (Europe/Oslo), inclusive. Default: today. Must not be before from, and the window can be at most 1096 days. |
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"{
"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
}
}
}Stiler
get/v1/stylesList stiler
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
curl "https://api.qsy.no/v1/styles" \
-H "Authorization: Bearer $QSY_API_KEY"{
"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}Hent stil
One saved style.
Scope styles:read
Parametre
| Navn | I | Type | Påkrevd | Beskrivelse |
|---|---|---|---|---|
id | sti | string · uuid | ja | Style id. A value that is not a UUID → 404. |
curl "https://api.qsy.no/v1/styles/9c8b7a6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d" \
-H "Authorization: Bearer $QSY_API_KEY"{
"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}/previewForhåndsvis en stil (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
Parametre
| Navn | I | Type | Påkrevd | Beskrivelse |
|---|---|---|---|---|
id | sti | string | ja | Style id (UUID), or "default". |
format | spørring | "svg" | nei | Only svg. |
size | spørring | integer | nei | Pixel size of the SVG coordinate system, clamped to 128-1024 (default 512; invalid → 512). |
curl "https://api.qsy.no/v1/styles/id/preview" \
-H "Authorization: Bearer $QSY_API_KEY" \
-o qr.svg<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">…</svg>API-et følger med Bedrift.
Med Bedrift lager du nøkler under Konto og er i gang med en gang. Hva Bedrift ellers rommer, står på prissiden.