{"openapi":"3.1.0","info":{"title":"QR Suisse public API — v1","version":"1.1.1","description":"Public REST API of QR Suisse, available with the Pro and Entreprise plans. Documents the\n8 endpoints exposed under `/api/v1/*`, in their version 1.1.1. Within v1, changes are\ncompatible additions, apart from a few changes in behaviour listed in \"Version history\".\n\n## Authentication\n\nHeader `Authorization: Bearer qrs_live_<apiKeyId>.<secret>` — a single string copied as is\nfrom the reveal panel of the dashboard (when the key is created or rotated), to be pasted\ndirectly into the header. **This is not a JWT**: the part before the first `.` (after the\nfixed `qrs_live_` prefix) is the key identifier (direct lookup, never secret), the part after\nthe `.` is the opaque, high-entropy secret, compared in constant time on the server. A\nmissing or malformed header, a revoked or unknown key, a frozen key (see below) or a wrong\nsecret **all** return the same `401 unauthorized` error — none of these cases is\ndistinguished in the response (anti-enumeration). Every `401` carries the same\n`WWW-Authenticate: Bearer realm=\"qrsuisse-api\"` header (RFC 6750), with no detail on the\nreason for the refusal.\n\nThe scheme name, `Bearer`, is case-insensitive (RFC 9110); it is followed by a single space,\nthen the key. The key itself is case-sensitive: send it exactly as it was copied.\n\n**HTTPS only; a key sent over HTTP must be rotated.** The API answers only over HTTPS. A\nrequest sent over HTTP (`http://`) is neither redirected nor processed: it receives\n`403 https_required`, before any check of the key. By then the key has already travelled in\nclear text over the network: treat it as exposed and rotate it from the dashboard (see\n\"Rotation and revocation\").\n\nAccess to the API as a whole is a capability of the **Pro and Entreprise plans**, checked on\nevery request (never cached). Each plan allows a maximum number of keys active at the same\ntime: **1 with the Pro plan, 10 with the Entreprise plan**. When a plan change lowers this\nlimit, the excess keys (the most recent ones) are **frozen** — neither revoked nor\ndeleted — and become usable again automatically as soon as a plan allowing more keys is\nactive again. When a Pro or Entreprise subscription ends and the account goes back to the\nFree plan, **all** its keys are frozen: they stop working immediately, read access\nincluded, with `401 unauthorized`. `403 plan_required` only concerns a key that is not\nfrozen, on an account that is no longer on a Pro or Entreprise plan (edge case, outside the\nnormal end of a subscription).\n\n## Obtaining a key\n\nAn API key can only be created from the QR Suisse dashboard, by a member of a Pro or Entreprise\naccount who has the \"Manager of API keys\" permission (or Administrator); this member also\nchooses the key's rights. There is no OAuth, no automatic client registration and no API\nendpoint that issues a key: an agent or a tool cannot obtain a key on its own; this member\nprovides it. An agent without a key therefore asks the user to have one created with the rights\nit needs and to provide it through the agent's configuration. The full key is only shown once,\nwhen it is created or rotated: QR Suisse does not store it in plain text and cannot send it\nagain.\n\n## Suspended account or account pending deletion\n\nWhen the account that owns the key is **suspended** by QR Suisse, or **pending deletion**\ndue to inactivity, any request with a valid key returns `403 account_disabled`, **read\naccess included**. The message states which of the two applies, never the reason for the\nsuspension. This check happens after the key is verified and before the plan and rights are\nchecked: an invalid, revoked or frozen key still gets `401 unauthorized`. The key is neither\nrevoked nor frozen: there is no point in regenerating it, the account holder has to take\naction. When a suspension is lifted, the key works again automatically, without rotation.\nPending deletion only concerns accounts on the Free plan: after reactivation, the account\nstays on the Free plan and the usual rules apply (frozen key, hence `401 unauthorized`, or\n`403 plan_required`).\n\n## Atomic rights of a key\n\nA key carries a subset of the following 5 atomic rights: `QR_CREATE`, `QR_READ`, `QR_UPDATE`,\n`QR_DELETE`, `STATS_READ`. Each operation states the right it requires (\"Required right\") — it\nis **not** modelled with OAuth2 scopes (this API has a single Bearer authentication scheme, not\na standard scope mechanism). A call with an authenticated key that lacks the required right\nreturns `403 forbidden`.\n\n## Multi-tenant isolation\n\nEach key only acts on the resources of the account that issued it. A resource that does not\nexist OR belongs to another account **always** returns `404 not_found` — never `403`: the\nAPI never confirms that a resource of another account exists.\n\n## Rotation and revocation\n\n**Rotating** a key, from the dashboard, generates a new secret and invalidates the old one at\nthe same instant: a key never has two valid secrets at the same time. Only the secret changes:\nthe key keeps its identifier (the part before the `.`), its name and its rights; until the tool\nthat uses it receives the new value, its calls are refused with `401 unauthorized`. **Revoking**\na key makes it unusable immediately and permanently (`401 unauthorized`): it can no longer be\nreactivated or rotated. QR Suisse can also revoke all the keys of an account, active or frozen,\nat the request of one of the account's administrators or if a key may have been exposed or used\nwithout the account holder's consent; the account's administrators are informed by email.\nNeither rotation nor revocation changes or deletes any QR code.\n\n## Keeping the key secret\n\n- Treat the full key as a password: store it in a secrets manager or an environment variable,\n  never in versioned code, a shared file, a ticket or a screenshot.\n- Never paste the key into a shared or stored conversation, including with an AI assistant. An\n  agent receives the key through its configuration (environment variable, secrets manager) and\n  never asks the user to type it into the conversation.\n- Send it only in the `Authorization` header, over HTTPS, to `https://app.qrsuisse.ch/api/v1` —\n  never in a URL. Do not log this header.\n- Give a key only the rights it needs: for example `QR_READ` and `STATS_READ` (the \"AI assistant\n  agent\" preset in the dashboard) for an agent that only looks things up. With the Entreprise\n  plan, use one key per tool, so that you can revoke one without cutting off the others.\n- If the key may have been exposed, rotate or revoke it immediately from the dashboard; if the\n  account is on the Free plan, write to contact@qrsuisse.ch from the email address of one of the\n  account's administrators. An agent that suspects its key may have been exposed reports it to\n  the account holder.\n\n## Rate limiting\n\nThree independent buckets:\n- **Generic** — 30 requests / 60 seconds per API key, applied to **every** authenticated\n  request on the 8 endpoints.\n- **Creation** — 8 requests / 60 seconds per API key, applied **on top of** the generic\n  bucket, only on `POST /qr-codes`.\n- **PNG image** — 10 renders / 60 seconds **per account** (shared by all the keys of the\n  account), applied **on top of** the generic bucket, only on `GET /qr-codes/{id}/image` in\n  PNG format. Neither the SVG format nor a `304` response counts towards it.\n\nExceeding a limit returns `429 rate_limited` with a `Retry-After` header (seconds). The\nbuckets of a key are counted as soon as the key and its secret are recognised, including\nwhen the request is then refused (`403 account_disabled`, `plan_required` or `forbidden`):\nonce a bucket is exhausted, `429 rate_limited` is returned, before these refusals.\n\nRequests refused with `401 unauthorized` are additionally limited by IP address:\n60 refusals / 60 seconds per IPv4 address or per IPv6 /64 network, and 300 refusals /\n60 seconds per IPv6 /48 network. Beyond that, the API returns `429 rate_limited` (with\n`Retry-After`) without checking the key, even a valid one, until the end of the window.\n\n## Commercial creation quota\n\n`POST /qr-codes` is also subject to a monthly creation quota: **5 creations per UTC\ncalendar month with the Pro plan, 100 with the Entreprise plan**, then the balance of the\npurchased API creation packs (one-time purchase, used after the monthly quota, oldest\nfirst). Each successful creation counts, even if the QR code is deleted afterwards:\ndeleting does not give a creation back. The monthly quota does not carry over and resets on the 1st of each month at\n00:00 UTC. When the monthly quota and all available packs are used up, creation returns\n`429 quota_exceeded` with the reset date of the monthly quota (`resetAt`, ISO 8601) — hard\nblock, no pay-per-use billing in v1.\n\n`GET /usage` gives the balance at any time: creations of the month, monthly quota, credits\nremaining in the packs and reset date.\n\n## Common error format\n\nEvery error of this API returns the same shape `{ \"error\": \"<code>\", \"message\": \"<text>\",\n...possible details }`. 14 stable codes: an existing code never changes; a minor version may\nadd new ones, only for a new and optional feature (see \"Version history\"). The codes:\n`unauthorized` (401),\n`plan_required` (403), `forbidden` (403), `account_disabled` (403), `https_required` (403),\n`not_found` (404), `method_not_allowed` (405), `validation_error` (400), `quota_exceeded`\n(429), `rate_limited` (429), and for `POST /qr-codes` with the `Idempotency-Key` header:\n`idempotency_key_mismatch` (422), `idempotency_in_progress` (409), `resource_deleted` (410),\nand for `POST /qr-codes` and `PATCH /qr-codes/{id}`: `payload_too_large` (413), request body\nlarger than 64 KiB.\nThe `message` text is a reading aid, in French as in the examples of this specification, and\nit may change without notice. Base your code on the `error` code, never on `message`.\nA URL path containing an invalid `%` sequence (e.g. `%zz`) is refused before any\nauthentication with `400 validation_error`. An unknown path under `/api/v1/` returns\n`404 not_found`. An HTTP method that an existing endpoint does not support returns\n`405 method_not_allowed`, with an `Allow` header listing the accepted methods. These two\nresponses are the same with or without an API key. A request over HTTP receives\n`403 https_required` before all these checks (see \"Authentication\").\n\n## Public index\n\n`GET /api/v1`, without authentication, returns a static JSON document: name and version of\nthe API, and links to its OpenAPI specification, its catalog (RFC 9727), its documentation\nand its authentication guide. The specification is published at\n`https://www.qrsuisse.ch/openapi.json` (and `/openapi.yaml`), in English by default;\n`?lang=fr`, `de` or `it` returns the translated version. The index is limited to 60 requests\n/ 60 seconds per IPv4 address or per IPv6 /64 network, and to 300 requests / 60 seconds per\nIPv6 /48 network (`429 rate_limited` beyond that).\n\nThe authentication guide, in English, is published at `https://www.qrsuisse.ch/auth.md`.\n\n## Good practices\n\n- Send an `Idempotency-Key` with every creation (a new random value, UUID v4 recommended, no\n  personal data) and send the same value again if the request fails on a timeout or a dropped\n  connection (see `POST /qr-codes`).\n- On `429 rate_limited` or `409 idempotency_in_progress`, wait for the delay given in\n  `Retry-After` before trying again; on `429 quota_exceeded`, do not try again before\n  `resetAt` or the purchase of a pack (the balance can be read with `GET /usage`).\n- To page through a filtered or sorted list, send the same filter and sort parameters again\n  with `cursor`; only `limit` may change.\n- In a date with an offset (`createdFrom`, `createdBefore`), encode the `+` as `%2B`, or use\n  `Z`.\n- Send the `ETag` of an image back in `If-None-Match` to avoid downloading it again, and prefer\n  SVG for printing.\n- Handle errors by their `error` code, never by `message`.\n\n## Version history\n\nThe version is given in `info.version` of this specification. Within v1, a minor version only\nadds optional features: an integration written for v1.0 keeps working without any change,\nsubject to the \"Changes in behaviour\" below. A patch version (for example 1.1.1) fixes a\ndefect, without new features; it may reject a request that did not comply with this\ndocumentation. An incompatible change would be released as a v2, at another address.\n\n### 1.1.1 — October 2026\n\nPatch version, without new features.\n\nChanges in behaviour:\n- A body of `POST /qr-codes` or `PATCH /qr-codes/{id}` that is not valid UTF-8 (for example\n  text encoded in ISO-8859-1) is rejected with `400 validation_error`. Previously, the invalid\n  bytes were replaced with \"�\" and saved.\n- Two simultaneous `DELETE` requests on the same QR code could still answer `500`, and a\n  `PATCH` on a QR code deleted at the same moment could answer `200` without saving anything,\n  or `500`: the request that finds the QR code already deleted now answers `404 not_found`, as\n  announced in 1.1.0.\n\n### 1.1.0 — October 2026\n\nNew:\n- **Archive and reactivate** a QR code with `PATCH /qr-codes/{id}` and the `active` field\n  (`false` archives, `true` reactivates), without using a creation. Right `QR_UPDATE`.\n- **Filter, search and sort the list** `GET /qr-codes`: `active`, `type`, `q` (label,\n  destination and end of the public URL), `createdFrom`, `createdBefore`, `sort`. For the next\n  page, send the same parameters again with `cursor`.\n- **Read the creation quota** with `GET /usage`: creations of the month, monthly quota, pack\n  credits remaining, reset date. Right `QR_CREATE`.\n- **Retry a creation without a duplicate** with the optional `Idempotency-Key` header on\n  `POST /qr-codes` (UUID v4 recommended, no personal data, 24 hours). Three new error codes,\n  returned only with this header: `422 idempotency_key_mismatch`,\n  `409 idempotency_in_progress`, `410 resource_deleted`.\n\nChanges in behaviour:\n- `GET /qr-codes` without parameters sorts by creation date, newest first, then by `id`\n  (before: by `id` descending only); in practice the order is the same.\n- `nextCursor` is now an opaque value. A cursor that is invalid, obtained with other\n  parameters or by another account is rejected with `400 validation_error`. The former v1.0\n  cursor (the `id` of the last item) remains accepted throughout v1, for the list without\n  filter or sort.\n- The `active`, `type`, `q`, `createdFrom`, `createdBefore` and `sort` parameters of the list,\n  previously ignored, are now applied and validated: an invalid, empty or repeated value is\n  rejected with `400 validation_error`.\n- An `active` field in the body of `PATCH /qr-codes/{id}`, previously ignored, is now applied.\n- A request body larger than 64 KiB on `POST /qr-codes` or `PATCH /qr-codes/{id}` is rejected\n  with `413 payload_too_large` (new code; a normal body comes nowhere near this limit).\n- A label containing a control character (null character, tab, line break…) is rejected with\n  `400 validation_error`.\n- Every response carries `Cache-Control: no-store`, except the image of a QR code\n  (`private, no-cache` with `ETag`).\n- A `PATCH` or `DELETE` on a QR code deleted at the same moment by another request answers\n  `404 not_found` instead of `500`.\n- A malformed Unicode string (an isolated surrogate, for example `\\ud800`) in the body of\n  `POST /qr-codes` or `PATCH /qr-codes/{id}` is rejected with `400 validation_error` instead of\n  a `500` error.\n\n### 1.0.0 — September 2026\n\nFirst public version: creating, reading, listing, updating and deleting QR codes (`URL` and\n`VCARD` types), PNG or SVG image, scan statistics.\n","contact":{"name":"QR Suisse"}},"servers":[{"url":"https://app.qrsuisse.ch/api/v1","description":"Production"}],"security":[{"ApiKeyAuth":[]}],"tags":[{"name":"QR Codes","x-displayName":"QR codes","description":"Manage the QR codes of the account that owns the API key (create, search, update, archive, delete), download their image and read their statistics."},{"name":"Usage","x-displayName":"Creation quota","description":"Balance of the API creation quota of the account that owns the API key."}],"paths":{"/qr-codes":{"get":{"operationId":"listQrCodes","tags":["QR Codes"],"summary":"List the QR codes of the account","description":"Required right: **`QR_READ`**. No quota (read-only).\n\nWithout parameters: all QR codes of the account, active and archived, newest first\n(descending creation date, then `id`).\n\nOptional filters that can be combined: status (`active`), type (`type`), search (`q`),\ncreation period (`createdFrom`, `createdBefore`), sort order (`sort`). The search covers\nthe label, the destination and the end of the public URL (`slug`), never the fields of a\ncontact card. An invalid value for one of these parameters is rejected with `400\nvalidation_error` (the message names the parameter), never replaced by a default value:\nan integration never receives, without knowing it, a list other than the one it asked\nfor. A parameter given twice, or present but empty (`?active=`), is rejected the same\nway; only an empty `cursor` (first page) and `limit` are exceptions. An unknown parameter\nname is ignored.\n\nOpaque cursor pagination (`nextCursor`). For the next page, send **the same** filter and\nsort parameters again, with `cursor`; only `limit` may change. A cursor obtained with\nother parameters, or by another account, is rejected with `400`. The `accountId` filter\nof the key remains the only source of what is returned.\n","parameters":[{"$ref":"#/components/parameters/ActiveFilterParam"},{"$ref":"#/components/parameters/TypeFilterParam"},{"$ref":"#/components/parameters/SearchParam"},{"$ref":"#/components/parameters/CreatedFromParam"},{"$ref":"#/components/parameters/CreatedBeforeParam"},{"$ref":"#/components/parameters/SortParam"},{"$ref":"#/components/parameters/LimitParam"},{"$ref":"#/components/parameters/CursorParam"}],"responses":{"200":{"description":"Page of results.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrCodeListPage"}}}},"400":{"description":"Invalid value for a filter, sort or cursor parameter, repeated parameter, or cursor\nobtained with other parameters or whose last QR code has since been deleted or renamed\n(label sort). The message names the parameter, never its value.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrPlanRequired"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"operationId":"createQrCode","tags":["QR Codes"],"summary":"Create a QR code","description":"Required right: **`QR_CREATE`**. Subject to the commercial creation quota (monthly quota\nof the plan: 5 with Pro, 100 with Entreprise, then balance of the API creation packs) —\nchecked in the same transaction as the write, against a concurrent creation close to the\nlimit. A creation still counts if the QR code is deleted afterwards.\n\n`type` is fixed at creation (`URL` or `VCARD`, discriminator of the request body) and can\n**never** be changed afterwards. For `VCARD`, `phones`/`emails`/`addresses` are each\noptional — when missing, they are treated as empty arrays (a minimal card with first name\nand last name is a valid use case).\n\nUses the generic rate limit bucket **and** the dedicated `api-create` bucket (8/min, on\ntop of the generic bucket of 30/min).\n\n**Retries without duplicates.** With the optional `Idempotency-Key` header, a request\nsent again after a network failure or a timeout does not create a second QR code and\ndoes not use a second creation of the quota. Use a new random value for each creation\n(UUID v4 recommended) and never put personal data in it. Behaviour:\n- same value and same body within 24 hours: `201` with the QR code already created, in\n  its current state, and the `Idempotent-Replayed: true` header; nothing is deducted\n  from the quota. This replay uses the rate limit buckets like a creation;\n- same value with a different body: `422 idempotency_key_mismatch`, nothing is created;\n- same value while the first request is still in progress: the second one normally waits\n  for it to finish and gets the same QR code; if the wait is too long,\n  `409 idempotency_in_progress` with `Retry-After`;\n- the QR code created with this value has been deleted since: `410 resource_deleted`,\n  nothing is created again;\n- a refused request (`400`, `403`, `429`, server error) is not kept: the same value can be\n  used again once the problem is fixed;\n- after 24 hours, the value is forgotten: a new request with the same value creates a new QR\n  code and uses a creation.\n\nThe `Idempotency-Key` value is specific to each API key: the same value sent with another API key creates\nanother QR code. Only non-reversible fingerprints of the value and of the body are stored,\nwith the identifier of the QR code created, and taken into account for 24 hours.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKeyHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrCodeCreateInput"}}}},"responses":{"201":{"description":"QR code created, or creation replayed with the same `Idempotency-Key`.","headers":{"Idempotent-Replayed":{"description":"`true` when the response replays a creation already made with the same\n`Idempotency-Key` (no new creation, no quota used). Absent otherwise.\n","schema":{"type":"string","enum":["true"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrCodeApiDto"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrPlanRequired"},"409":{"$ref":"#/components/responses/IdempotencyInProgress"},"410":{"$ref":"#/components/responses/ResourceDeleted"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"422":{"$ref":"#/components/responses/IdempotencyKeyMismatch"},"429":{"$ref":"#/components/responses/RateLimitedOrQuotaExceeded"}}}},"/qr-codes/{id}":{"parameters":[{"$ref":"#/components/parameters/QrCodeIdParam"}],"get":{"operationId":"getQrCode","tags":["QR Codes"],"summary":"Get a QR code","description":"Required right: **`QR_READ`**. No quota.\n","responses":{"200":{"description":"QR code found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrCodeApiDto"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrPlanRequired"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}},"patch":{"operationId":"updateQrCode","tags":["QR Codes"],"summary":"Partially update a QR code","description":"Required right: **`QR_UPDATE`**. No creation quota: neither an update nor a reactivation\n(`active: true`) uses up a credit.\n\n**Archiving and reactivating**: `active: false` archives the QR code — its public redirect\nthen answers `404` (\"QR code introuvable ou désactivé.\"), exactly as after archiving it\nfrom the dashboard — and `active: true` reactivates it. `active` must be a JSON boolean: `\"false\"`,\n`0` or `null` are refused with `400 validation_error`. If the body only contains `active`\nand the QR code is already in the requested state, the response is `200` with nothing\nwritten (`updatedAt` unchanged).\n`active` can be combined with the other fields: the whole body is validated before\nanything is written, then applied in one go — a `400` error applies nothing, neither the\narchiving nor the other fields.\n\n`type` can **never** be changed (a `\"type\"` key in the body is silently ignored, it is\nnever a `400` error) — it is always the type already stored that determines which fields\nof the request body below actually apply. Fields that do not concern the existing type\nof the resource are silently ignored in the same way (never rejected).\n\nSemantics of a partial PATCH: a field missing from the JSON body leaves the current value\nunchanged. For `organization`/`jobTitle`/`website` (VCARD), a field that is **present**\nwith an empty string explicitly clears the value (set to `null`) — what counts is its\npresence in the JSON body, not only its value. For `phones`/`emails`/`addresses`\n(VCARD), an array that is **present** (even empty) replaces the whole category.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrCodeUpdateInput"}}}},"responses":{"200":{"description":"QR code updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QrCodeApiDto"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrPlanRequired"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"operationId":"deleteQrCode","tags":["QR Codes"],"summary":"Permanently delete a QR code","description":"Required right: **`QR_DELETE`**. No quota. Deleting does not give the creation back to\nthe monthly quota.\n\n**Actual** deletion (not archiving): the phones, emails, addresses and daily statistics\nof the QR code are deleted with it, and its slug is permanently retired at the same time\n— a medium already printed with this slug never starts pointing to a different QR code\ncreated later.\n","responses":{"204":{"description":"QR code deleted, no response body."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrPlanRequired"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/qr-codes/{id}/stats":{"parameters":[{"$ref":"#/components/parameters/QrCodeIdParam"}],"get":{"operationId":"getQrCodeStats","tags":["QR Codes"],"summary":"Get the scan statistics of a QR code","description":"Required right: **`STATS_READ`**. No quota.\n\n`dailyStats` may have gaps: only the days with at least one scan have an entry (days\nwithout a scan are not filled with 0, unlike the dashboard chart) — an API integrator\nconsumes raw data, not a presentation. Window limited to the history of the Pro and\nEntreprise plans (15 months) — access to the API itself is only\npossible with a Pro or Entreprise plan.\n","responses":{"200":{"description":"Statistics of the QR code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsApiDto"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrPlanRequired"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/qr-codes/{id}/image":{"parameters":[{"$ref":"#/components/parameters/QrCodeIdParam"}],"get":{"operationId":"getQrCodeImage","tags":["QR Codes"],"summary":"Get the image of a QR code (PNG or SVG)","description":"Required right: **`QR_READ`**. No quota.\n\nReturns the image file of the QR code, rendered on the server with exactly the same\nsettings as the dashboard (black on white, error correction level M, margin of 4\nmodules): a QR code downloaded through the API is identical to the one from the\ndashboard. No visual option (colour, logo, margin) can be set in this version.\n\nThe encoded content is the public URL of the QR code (`publicUrl` of\n`GET /qr-codes/{id}`): on the custom domain of the account if it is active, on the\nstandard domain otherwise. The image is served even if the QR code is deactivated\n(`active: false`).\n\nThe file is returned as an attachment (`Content-Disposition: attachment`), named\n`qr-suisse-<slug>.png` or `qr-suisse-<slug>.svg`. The response carries an `ETag`:\nsending its value back in `If-None-Match` returns `304 Not Modified` without a body if\nthe image has not changed (the call is still authenticated, counted in the generic rate\nlimit bucket and logged, but not in the bucket dedicated to PNG renders).\n\nRate limiting: on top of the generic bucket (30 requests / 60 s per key), each PNG render\nuses the dedicated bucket of **10 renders / 60 s per account**, shared by all the keys of\nthe account. SVG renders do not use this dedicated bucket.\nThe `ETag` changes in particular when the custom domain of the account is activated or\ndeactivated.\n","parameters":[{"name":"format","in":"query","required":false,"description":"Image format, in lower case. Any other value returns `400 validation_error` (never a\nsilent fallback to another format).\n","schema":{"type":"string","enum":["png","svg"],"default":"png"}},{"name":"size","in":"query","required":false,"description":"PNG only: side of the square in pixels, margin included, integer from 128 to 1024. A\nnon-integer or out-of-range value returns `400 validation_error`. Ignored for SVG\n(vector, can be resized without loss — the format to prefer for large-format\nprinting).\n","schema":{"type":"integer","minimum":128,"maximum":1024,"default":1024}},{"name":"If-None-Match","in":"header","required":false,"description":"`ETag` value obtained from a previous call.","schema":{"type":"string"}}],"responses":{"200":{"description":"Image of the QR code.","headers":{"ETag":{"description":"Fingerprint of the image (format, size and encoded content).","schema":{"type":"string"}},"Content-Disposition":{"description":"`attachment; filename=\"qr-suisse-<slug>.<png|svg>\"`","schema":{"type":"string"}},"Cache-Control":{"description":"`private, no-cache` — authenticated response, never stored in a shared cache, revalidated on every use.","schema":{"type":"string"}}},"content":{"image/png":{"schema":{"type":"string","format":"binary"}},"image/svg+xml":{"schema":{"type":"string"}}}},"304":{"description":"The image has not changed since the `ETag` sent in `If-None-Match` — no body.","headers":{"ETag":{"description":"Fingerprint of the image, identical to the one sent.","schema":{"type":"string"}}}},"400":{"description":"Invalid `format` or `size` parameter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrPlanRequired"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"description":"`rate_limited` — generic bucket exceeded (30 requests / 60 s per API key), too many\nrequests refused with `401` from the same IP address (60 / 60 s, or 300 / 60 s per\nIPv6 /48 network), or, for the PNG format, bucket dedicated to renders exceeded\n(10 renders / 60 s per account).\n","headers":{"Retry-After":{"description":"Estimated number of seconds until the current window frees up.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitedError"}}}}}}},"/usage":{"get":{"operationId":"getUsage","tags":["Usage"],"summary":"Get the API creation quota of the account","description":"Required right: **`QR_CREATE`**. No quota: this read does not use any creation.\n\nReturns the figures shown at the same moment on the \"API keys\" screen of the dashboard,\nfor the account that owns the key: API creations in the current UTC calendar month,\nmonthly quota of the plan, credits remaining in the API creation packs and reset date of\nthe monthly quota. A successful creation increases `used` by 1; a deletion does not\nlower it. Beyond the monthly quota, each creation also uses a pack credit.\n\nIndicative values: a creation made at the same moment with another key of the account\ncan make them inaccurate as soon as they are read. Only `POST /qr-codes` checks the quota\nwith certainty (see \"Commercial creation quota\").\n","responses":{"200":{"description":"API creation quota of the account.","headers":{"Cache-Control":{"description":"`no-store` — the response changes with every creation, it is never cached.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageApiDto"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/ForbiddenOrPlanRequired"},"429":{"$ref":"#/components/responses/RateLimited"}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"qrs_live_<apiKeyId>.<secret>","description":"Opaque two-part token, **not a JWT**: `qrs_live_<apiKeyId>.<secret>`, sent in\n`Authorization: Bearer <token>`.\n"}},"parameters":{"QrCodeIdParam":{"name":"id","in":"path","required":true,"description":"Identifier (`cuid`) of the QR code.","schema":{"type":"string"}},"LimitParam":{"name":"limit","in":"query","required":false,"description":"Maximum number of items per page. Default 20. A missing, non-integer or ≤ 0 value falls\nback silently to the default (convenience parameter, never a `400` error); a value above\n100 is capped at 100.\n","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},"CursorParam":{"name":"cursor","in":"query","required":false,"description":"Value of `nextCursor` received with the previous page, to send back as is with the same\nfilter and sort parameters. Opaque value: do not infer anything from it, its format may\nchange. Missing for the first page. A cursor that is invalid, obtained with other\nparameters or by another account is rejected with `400`. The former v1.0 cursor (the\n`id` of the last item) remains accepted throughout v1, for the list without filter or\nsort.\n","schema":{"type":"string","maxLength":512}},"ActiveFilterParam":{"name":"active","in":"query","required":false,"description":"`true`: active QR codes only; `false`: archived QR codes only. Missing: both.\nExactly `true` or `false`, in lower case.\n","schema":{"type":"string","enum":["true","false"]}},"TypeFilterParam":{"name":"type","in":"query","required":false,"description":"One or more types separated by commas (at most 8 values, upper or lower case): `url` (all\nlinks, `type` `URL`), `web` (web page), `email` (`mailto:` destination), `phone`\n(`tel:`), `sms` (`sms:`), `ftp` (`ftp:`), `calendar` (calendar event), `vcard` (contact\ncard). A QR code is returned if it is of one of the given types.\n","style":"form","explode":false,"schema":{"type":"array","maxItems":8,"items":{"type":"string","enum":["url","web","email","phone","sms","ftp","calendar","vcard"]}},"example":["vcard","email"]},"SearchParam":{"name":"q","in":"query","required":false,"description":"Search in the label, the destination and the end of the public URL (`slug`), ignoring\ncase and accents (\"geneve\" finds \"Genève\"). Never in the fields of a contact card. 1 to\n100 characters once leading and trailing spaces are removed, no control character; `%`\nand `_` are searched literally.\n","schema":{"type":"string","minLength":1,"maxLength":100}},"CreatedFromParam":{"name":"createdFrom","in":"query","required":false,"description":"QR codes created from this date, included. ISO 8601: `YYYY-MM-DD` (midnight UTC) or\ndate-time with `Z` or an offset. In the URL, encode the `+` of an offset as `%2B`\n(`createdFrom=2026-10-01T08:00:00%2B02:00`) or use `Z`\n(`createdFrom=2026-10-01T06:00:00Z`); an unencoded `+`, received as a space, is also\naccepted right before the offset. Year from 1970 to 9999, computed in UTC once the offset\nis applied; a date that does not exist (`2026-02-30`) or is outside this range is rejected\nwith `400 validation_error`.\n","schema":{"type":"string"},"example":"2026-10-01"},"CreatedBeforeParam":{"name":"createdBefore","in":"query","required":false,"description":"QR codes created before this date, excluded (same format as `createdFrom`). With\n`createdFrom=2026-10-01&createdBefore=2026-11-01`: the whole month of October (UTC).\n`createdFrom` must be earlier than `createdBefore`.\n","schema":{"type":"string"},"example":"2026-11-01"},"SortParam":{"name":"sort","in":"query","required":false,"description":"Order of the list: `created_desc` (default, newest first), `created_asc`, `label_asc`\n(label from A to Z), `label_desc`. Ties are broken by `id`. With `label_asc` or\n`label_desc`, if the last QR code of a page is deleted or renamed before the next call,\nthe next page returns `400` (start the pagination again from the first page) rather than\nsilently skipping QR codes: for a complete synchronisation,\nprefer `created_desc` or `created_asc`.\n","schema":{"type":"string","enum":["created_desc","created_asc","label_asc","label_desc"],"default":"created_desc"}},"IdempotencyKeyHeader":{"name":"Idempotency-Key","in":"header","required":false,"description":"Key chosen by the client for one logical creation, which makes it possible to send the\nsame request again without creating a duplicate (see `POST /qr-codes`). 1 to 255 visible\nASCII characters, no spaces; enclosing double quotes are ignored. UUID v4 recommended,\nno personal data. A malformed value returns `400 validation_error`.\n","schema":{"type":"string","minLength":1,"maxLength":255,"pattern":"^[\\x21-\\x7E]{1,255}$"},"example":"3f1c2b0e-8a4d-4c6e-9b7a-1d2e3f4a5b6c"}},"schemas":{"QrCodeType":{"type":"string","enum":["URL","VCARD"],"description":"Fixed at creation, can never be changed afterwards."},"PhoneType":{"type":"string","enum":["PROFESSIONAL","MOBILE","FAX"]},"EmailType":{"type":"string","enum":["PROFESSIONAL","PERSONAL"]},"AddressType":{"type":"string","enum":["PROFESSIONAL","HOME"]},"PhoneEntry":{"type":"object","required":["type","value"],"properties":{"type":{"$ref":"#/components/schemas/PhoneType"},"value":{"type":"string","minLength":1,"maxLength":255,"description":"Phone number as entered (no normalised format imposed)."}},"description":"At most 3 phone numbers per VCARD QR code."},"EmailEntry":{"type":"object","required":["type","value"],"properties":{"type":{"$ref":"#/components/schemas/EmailType"},"value":{"type":"string","format":"email","maxLength":255}},"description":"At most 2 email addresses per VCARD QR code."},"AddressEntry":{"type":"object","required":["type"],"properties":{"type":{"$ref":"#/components/schemas/AddressType"},"street":{"type":"string","maxLength":255},"complement":{"type":"string","maxLength":255},"postalCode":{"type":"string","maxLength":10},"city":{"type":"string","maxLength":255}},"description":"At most 2 postal addresses per VCARD QR code. The 4 fields are each\noptional (none is required for a valid entry).\n"},"VCardData":{"type":"object","required":["firstName","lastName","phones","emails","addresses"],"properties":{"firstName":{"type":"string","minLength":1,"maxLength":255},"lastName":{"type":"string","minLength":1,"maxLength":255},"organization":{"type":"string","maxLength":255},"jobTitle":{"type":"string","maxLength":255},"website":{"type":"string","maxLength":255},"phones":{"type":"array","items":{"$ref":"#/components/schemas/PhoneEntry"},"maxItems":3},"emails":{"type":"array","items":{"$ref":"#/components/schemas/EmailEntry"},"maxItems":2},"addresses":{"type":"array","items":{"$ref":"#/components/schemas/AddressEntry"},"maxItems":2}},"description":"Present only in `QrCodeApiDto.vcard` for `type === \"VCARD\"`. `organization`/\n`jobTitle`/`website` are left out of the response when they are not filled in (never an\nexplicit `null`) — an external integrator tests whether the key is present, not its\nvalue.\n"},"QrCodeApiDto":{"type":"object","required":["id","type","label","active","scanCount","slug","publicUrl","createdAt","updatedAt"],"properties":{"id":{"type":"string","description":"Identifier (`cuid`) of the QR code."},"type":{"$ref":"#/components/schemas/QrCodeType"},"label":{"type":"string"},"active":{"type":"boolean"},"scanCount":{"type":"integer","description":"Total number of scans since creation."},"slug":{"type":"string"},"publicUrl":{"type":"string","format":"uri","description":"Full public redirect URL (custom domain of the account if it is active, standard\ndomain otherwise) — never stored as is, always computed from the current state of\nthe account.\n"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"destinationUrl":{"type":"string","format":"uri","description":"Present only for `type === \"URL\"`, and only if it is filled in."},"vcard":{"allOf":[{"$ref":"#/components/schemas/VCardData"}],"description":"Present only for `type === \"VCARD\"`."}},"example":{"id":"cm2f8k3qz0001xyz9abcd1234","type":"URL","label":"Affiche salle d'attente","active":true,"scanCount":42,"slug":"AbC123_-xy","publicUrl":"https://app.qrsuisse.ch/r/AbC123_-xy","createdAt":"2026-10-01T08:30:00.000Z","updatedAt":"2026-10-12T14:05:00.000Z","destinationUrl":"https://www.exemple.ch/rendez-vous"},"description":"Public representation of a QR code: only the fields below are returned, no internal\naccount data.\n"},"QrCodeListPage":{"type":"object","required":["data","nextCursor"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/QrCodeApiDto"}},"nextCursor":{"type":["string","null"],"description":"Opaque cursor to pass as `?cursor`, with the same filter and sort parameters, to get\nthe next page; `null` if this is the last page.\n"}}},"QrCodeCreateUrlInput":{"type":"object","required":["type","label","destinationUrl"],"properties":{"type":{"type":"string","enum":["URL"]},"label":{"type":"string","minLength":1,"maxLength":255},"destinationUrl":{"type":"string","maxLength":2048,"description":"Destination URL — accepted protocols: `http:`/`https:` (no further restriction),\n`mailto:` (local part before a possible `?` validated as an e-mail address),\n`tel:`/`sms:` (digits/spaces/`+`/`-`/parentheses only, 4 to 20 characters, `*`/`#`\nexplicitly forbidden — risk of USSD code), `ftp:` (plain-text credentials in the\nauthority forbidden). No control character (literal or encoded as `%XX`).\n"}},"example":{"type":"URL","label":"Affiche salle d'attente","destinationUrl":"https://www.exemple.ch/rendez-vous"},"description":"Body of `POST /qr-codes` for a QR code of type `URL`."},"QrCodeCreateVcardInput":{"type":"object","required":["type","label","firstName","lastName"],"properties":{"type":{"type":"string","enum":["VCARD"]},"label":{"type":"string","minLength":1,"maxLength":255},"firstName":{"type":"string","minLength":1,"maxLength":255},"lastName":{"type":"string","minLength":1,"maxLength":255},"organization":{"type":"string","maxLength":255},"jobTitle":{"type":"string","maxLength":255},"website":{"type":"string","maxLength":255},"phones":{"type":"array","items":{"$ref":"#/components/schemas/PhoneEntry"},"maxItems":3,"default":[]},"emails":{"type":"array","items":{"$ref":"#/components/schemas/EmailEntry"},"maxItems":2,"default":[]},"addresses":{"type":"array","items":{"$ref":"#/components/schemas/AddressEntry"},"maxItems":2,"default":[]}},"example":{"type":"VCARD","label":"Carte de visite Dr Muster","firstName":"Anna","lastName":"Muster","organization":"Cabinet Muster","phones":[{"type":"PROFESSIONAL","value":"+41 27 123 45 67"}],"emails":[{"type":"PROFESSIONAL","value":"anna.muster@exemple.ch"}]},"description":"Body of `POST /qr-codes` for a QR code of type `VCARD`. Only `label`/`firstName`/\n`lastName` are mandatory — a minimal card (just a name) is a valid use case;\nmissing `phones`/`emails`/`addresses` are equivalent to an empty array.\n"},"QrCodeCreateInput":{"oneOf":[{"$ref":"#/components/schemas/QrCodeCreateUrlInput"},{"$ref":"#/components/schemas/QrCodeCreateVcardInput"}],"discriminator":{"propertyName":"type","mapping":{"URL":"#/components/schemas/QrCodeCreateUrlInput","VCARD":"#/components/schemas/QrCodeCreateVcardInput"}},"description":"Union discriminated by `type`, fixed at creation — same validation rules as the\ndashboard (reused as is, no parallel schema).\n"},"QrCodeUpdateInput":{"type":"object","properties":{"active":{"type":"boolean","description":"`false` archives the QR code, `true` reactivates it. Strict JSON boolean, applies to both types."},"label":{"type":"string","minLength":1,"maxLength":255},"destinationUrl":{"type":"string","maxLength":2048,"description":"Applies only if the existing resource is of type `URL` — see the description of the operation."},"firstName":{"type":"string","minLength":1,"maxLength":255},"lastName":{"type":"string","minLength":1,"maxLength":255},"organization":{"type":"string","maxLength":255,"description":"An empty string, if the field is present, explicitly clears the value (`null`)."},"jobTitle":{"type":"string","maxLength":255,"description":"An empty string, if the field is present, explicitly clears the value (`null`)."},"website":{"type":"string","maxLength":255,"description":"An empty string, if the field is present, explicitly clears the value (`null`)."},"phones":{"type":"array","items":{"$ref":"#/components/schemas/PhoneEntry"},"maxItems":3,"description":"If present (even empty), replaces the whole category."},"emails":{"type":"array","items":{"$ref":"#/components/schemas/EmailEntry"},"maxItems":2,"description":"If present (even empty), replaces the whole category."},"addresses":{"type":"array","items":{"$ref":"#/components/schemas/AddressEntry"},"maxItems":2,"description":"If present (even empty), replaces the whole category."}},"example":{"destinationUrl":"https://www.exemple.ch/nouvelle-page"},"description":"Body of `PATCH /qr-codes/{id}` — **all fields are optional** (partial update semantics:\na missing field leaves the current value unchanged). `type` **never** appears in this\nbody (cannot be changed, see the description of the operation): it is the type already\nstored that determines which of the fields `destinationUrl` (`URL`) or\n`firstName`/`lastName`/`organization`/`jobTitle`/`website`/`phones`/`emails`/`addresses`\n(`VCARD`) actually applies — fields that do not concern the existing type are silently\nignored, never rejected.\n"},"StatsApiDto":{"type":"object","required":["qrCodeId","scanCountTotal","dailyStats"],"properties":{"qrCodeId":{"type":"string"},"scanCountTotal":{"type":"integer","description":"Total number of scans since creation (`QrCode.scanCount`), not only over the `dailyStats` window."},"dailyStats":{"type":"array","items":{"type":"object","required":["date","count"],"properties":{"date":{"type":"string","format":"date-time"},"count":{"type":"integer"}}},"description":"One item per day with at least one scan (no filling with 0), limited to the history\nwindow of the Pro and Entreprise plans (15 months).\n"}},"example":{"qrCodeId":"cm2f8k3qz0001xyz9abcd1234","scanCountTotal":42,"dailyStats":[{"date":"2026-10-11T00:00:00.000Z","count":5},{"date":"2026-10-12T00:00:00.000Z","count":3}]},"description":"Public DTO of the statistics endpoint."},"UsageApiDto":{"type":"object","required":["used","monthlyQuota","packCreditsRemaining","resetAt"],"additionalProperties":false,"properties":{"used":{"type":"integer","minimum":0,"description":"API creations in the current UTC calendar month, including those that used a pack\ncredit and those of QR codes deleted since. Can therefore exceed `monthlyQuota`.\n"},"monthlyQuota":{"type":"integer","minimum":0,"description":"Creations included each month in the plan (5 with Pro, 100 with Entreprise)."},"packCreditsRemaining":{"type":"integer","minimum":0,"description":"Credits still available in the API creation packs of the account, all usable packs combined."},"resetAt":{"type":"string","format":"date-time","description":"Reset of the monthly quota — 1st day of the next UTC calendar month, at midnight UTC."}},"example":{"used":7,"monthlyQuota":5,"packCreditsRemaining":98,"resetAt":"2026-11-01T00:00:00.000Z"},"description":"Public DTO of the quota endpoint — these four fields only."},"ApiErrorBase":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","enum":["unauthorized","plan_required","forbidden","account_disabled","https_required","not_found","method_not_allowed","validation_error","quota_exceeded","rate_limited","idempotency_key_mismatch","idempotency_in_progress","resource_deleted","payload_too_large"]},"message":{"type":"string"}},"description":"Shape shared by every error of API v1."},"UnauthorizedError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"unauthorized","message":"Authentification invalide ou absente."}},"PlanRequiredError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"plan_required","message":"Cette fonctionnalité nécessite un plan Pro ou Entreprise."}},"ForbiddenError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"forbidden","message":"Cette clé API n'a pas le droit requis pour cette opération."}},"AccountDisabledError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"account_disabled","message":"Ce compte a été suspendu. Contactez le support QR Suisse à l'adresse contact@qrsuisse.ch pour plus d'informations."},"description":"Account suspended or pending deletion. Depending on the state, `message` (in French) is:\n- suspended: « Ce compte a été suspendu. Contactez le support QR Suisse à l'adresse\n  contact@qrsuisse.ch pour plus d'informations. » (This account has been suspended.\n  Contact QR Suisse support at contact@qrsuisse.ch for more information.)\n- pending deletion: « Ce compte est en attente de suppression pour inactivité.\n  Utilisez le lien de récupération reçu par email pour le réactiver. » (This account is\n  pending deletion due to inactivity. Use the recovery link received by e-mail to\n  reactivate it.)\n"},"HttpsRequiredError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"https_required","message":"L'API n'accepte que HTTPS : la requête n'a pas été traitée. Une clé envoyée en HTTP a circulé en clair, faites-la tourner depuis le tableau de bord."},"description":"Request received over HTTP: neither redirected nor processed, before any check of the\nkey. A key sent this way has travelled in clear text and must be rotated.\n"},"NotFoundError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"not_found","message":"Ressource introuvable."}},"ValidationError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"validation_error","message":"Le libellé est requis"},"description":"`message` repeats the first validation message encountered (e.g. a required field that\nis missing or too long) — the exact text depends on the field concerned, the `{ error, message }`\nshape always stays the same. This text is in French; a generic message names the field\nconcerned (e.g. \"Champ « type » requis : valeurs acceptées URL, VCARD.\"): only the `error`\ncode is guaranteed.\n"},"QuotaExceededError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["resetAt"],"properties":{"resetAt":{"type":"string","format":"date-time","description":"1st day of the next UTC calendar month, at midnight UTC — reset date of the quota."}}}],"example":{"error":"quota_exceeded","message":"Quota mensuel de créations API atteint.","resetAt":"2026-10-01T00:00:00.000Z"}},"RateLimitedError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"rate_limited","message":"Trop de requêtes pour cette clé API, réessayez plus tard."}},"IdempotencyKeyMismatchError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"idempotency_key_mismatch","message":"Cette clé Idempotency-Key a déjà servi pour une requête différente. Utilisez une nouvelle clé pour une nouvelle création."}},"IdempotencyInProgressError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"idempotency_in_progress","message":"Une requête avec cette clé Idempotency-Key est encore en cours de traitement, réessayez dans un instant."}},"ResourceDeletedError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"resource_deleted","message":"Le QR code créé avec cette clé Idempotency-Key a été supprimé depuis. Utilisez une nouvelle clé pour une nouvelle création."}},"PayloadTooLargeError":{"allOf":[{"$ref":"#/components/schemas/ApiErrorBase"}],"example":{"error":"payload_too_large","message":"Corps de la requête trop volumineux : 64 Kio maximum."}}},"responses":{"Unauthorized":{"description":"Missing/malformed `Authorization` header, unknown/revoked/frozen key, or wrong secret.","headers":{"WWW-Authenticate":{"description":"Always `Bearer realm=\"qrsuisse-api\"`, whatever the reason for the refusal.","schema":{"type":"string","const":"Bearer realm=\"qrsuisse-api\""}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnauthorizedError"}}}},"ForbiddenOrPlanRequired":{"description":"`403` — either the request was sent over HTTP (`https_required`, before any check of the\nkey), or the account is suspended or pending deletion (`account_disabled`, checked first\nfor a valid key), or the account is no longer on a Pro or Entreprise plan\n(`plan_required`), or the authenticated API key does not have the atomic right required\nfor this operation (`forbidden`).\n","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/HttpsRequiredError"},{"$ref":"#/components/schemas/AccountDisabledError"},{"$ref":"#/components/schemas/PlanRequiredError"},{"$ref":"#/components/schemas/ForbiddenError"}]}}}},"NotFound":{"description":"Resource missing, or belonging to another account (never distinguished from a plain absence).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}},"ValidationError":{"description":"Request body missing, not JSON, or not complying with the validation rules of the field concerned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"RateLimited":{"description":"Generic rate limit bucket exceeded (30 requests / 60 s per API key), or too many\nrequests refused with `401` from the same IP address (60 / 60 s, or 300 / 60 s per IPv6\n/48 network).\n","headers":{"Retry-After":{"description":"Estimated number of seconds until the current window frees up.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitedError"}}}},"RateLimitedOrQuotaExceeded":{"description":"`429` — either a rate limit bucket exceeded (generic 30/min, dedicated to creation\n8/min, or 60 `401` refusals/min from the same IP address, 300/min per IPv6 /48 network —\n`rate_limited`), or the commercial monthly quota of API creations reached\n(`quota_exceeded`, before any write to the database).\n","headers":{"Retry-After":{"description":"Present only for `rate_limited` — estimated number of seconds until the current window frees up.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/RateLimitedError"},{"$ref":"#/components/schemas/QuotaExceededError"}]}}}},"IdempotencyKeyMismatch":{"description":"`422` — the same `Idempotency-Key` has already been used, within the last 24 hours, for a\nrequest with a different body. Nothing is created, no quota is used.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdempotencyKeyMismatchError"}}}},"IdempotencyInProgress":{"description":"`409` — a request with the same `Idempotency-Key` is still in progress and waiting for\nits result took too long. Nothing is created by this request: send it again after\n`Retry-After`, it will get the QR code created by the first one.\n","headers":{"Retry-After":{"description":"Recommended delay before a new attempt, in seconds.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdempotencyInProgressError"}}}},"ResourceDeleted":{"description":"`410` — the QR code created with this `Idempotency-Key` has been deleted since. Nothing\nis created again: use a new key for a new creation.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResourceDeletedError"}}}},"PayloadTooLarge":{"description":"`413` — request body larger than 64 KiB (65,536 bytes): the request is refused, nothing is\nwritten.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayloadTooLargeError"}}}}}}}