# QR Suisse API: authentication

> How an agent or a tool authenticates to the QR Suisse public API (v1): a member of the account creates an API key in the dashboard and gives it to you; send it in every request as `Authorization: Bearer <key>`. This document is generated from the OpenAPI specification of the API, which prevails in case of doubt.

- API base URL: https://app.qrsuisse.ch/api/v1
- OpenAPI specification: https://www.qrsuisse.ch/openapi.json

## Authentication

Header `Authorization: Bearer qrs_live_<apiKeyId>.<secret>` — a single string copied as is
from the reveal panel of the dashboard (when the key is created or rotated), to be pasted
directly into the header. **This is not a JWT**: the part before the first `.` (after the
fixed `qrs_live_` prefix) is the key identifier (direct lookup, never secret), the part after
the `.` is the opaque, high-entropy secret, compared in constant time on the server. A
missing or malformed header, a revoked or unknown key, a frozen key (see below) or a wrong
secret **all** return the same `401 unauthorized` error — none of these cases is
distinguished in the response (anti-enumeration). Every `401` carries the same
`WWW-Authenticate: Bearer realm="qrsuisse-api"` header (RFC 6750), with no detail on the
reason for the refusal.

The scheme name, `Bearer`, is case-insensitive (RFC 9110); it is followed by a single space,
then the key. The key itself is case-sensitive: send it exactly as it was copied.

**HTTPS only; a key sent over HTTP must be rotated.** The API answers only over HTTPS. A
request sent over HTTP (`http://`) is neither redirected nor processed: it receives
`403 https_required`, before any check of the key. By then the key has already travelled in
clear text over the network: treat it as exposed and rotate it from the dashboard (see
"Rotation and revocation").

Access to the API as a whole is a capability of the **Pro and Entreprise plans**, checked on
every request (never cached). Each plan allows a maximum number of keys active at the same
time: **1 with the Pro plan, 10 with the Entreprise plan**. When a plan change lowers this
limit, the excess keys (the most recent ones) are **frozen** — neither revoked nor
deleted — and become usable again automatically as soon as a plan allowing more keys is
active again. When a Pro or Entreprise subscription ends and the account goes back to the
Free plan, **all** its keys are frozen: they stop working immediately, read access
included, with `401 unauthorized`. `403 plan_required` only concerns a key that is not
frozen, on an account that is no longer on a Pro or Entreprise plan (edge case, outside the
normal end of a subscription).

## Obtaining a key

An API key can only be created from the QR Suisse dashboard, by a member of a Pro or Entreprise
account who has the "Manager of API keys" permission (or Administrator); this member also
chooses the key's rights. There is no OAuth, no automatic client registration and no API
endpoint that issues a key: an agent or a tool cannot obtain a key on its own; this member
provides it. An agent without a key therefore asks the user to have one created with the rights
it needs and to provide it through the agent's configuration. The full key is only shown once,
when it is created or rotated: QR Suisse does not store it in plain text and cannot send it
again.

## Atomic rights of a key

A key carries a subset of the following 5 atomic rights: `QR_CREATE`, `QR_READ`, `QR_UPDATE`,
`QR_DELETE`, `STATS_READ`. Each operation states the right it requires ("Required right") — it
is **not** modelled with OAuth2 scopes (this API has a single Bearer authentication scheme, not
a standard scope mechanism). A call with an authenticated key that lacks the required right
returns `403 forbidden`.

### Required right per operation

| Operation | Endpoint | Required right |
|---|---|---|
| List the QR codes of the account | `GET /api/v1/qr-codes` | `QR_READ` |
| Create a QR code | `POST /api/v1/qr-codes` | `QR_CREATE` |
| Get a QR code | `GET /api/v1/qr-codes/{id}` | `QR_READ` |
| Partially update a QR code | `PATCH /api/v1/qr-codes/{id}` | `QR_UPDATE` |
| Permanently delete a QR code | `DELETE /api/v1/qr-codes/{id}` | `QR_DELETE` |
| Get the scan statistics of a QR code | `GET /api/v1/qr-codes/{id}/stats` | `STATS_READ` |
| Get the image of a QR code (PNG or SVG) | `GET /api/v1/qr-codes/{id}/image` | `QR_READ` |
| Get the API creation quota of the account | `GET /api/v1/usage` | `QR_CREATE` |

## Suspended account or account pending deletion

When the account that owns the key is **suspended** by QR Suisse, or **pending deletion**
due to inactivity, any request with a valid key returns `403 account_disabled`, **read
access included**. The message states which of the two applies, never the reason for the
suspension. This check happens after the key is verified and before the plan and rights are
checked: an invalid, revoked or frozen key still gets `401 unauthorized`. The key is neither
revoked nor frozen: there is no point in regenerating it, the account holder has to take
action. When a suspension is lifted, the key works again automatically, without rotation.
Pending deletion only concerns accounts on the Free plan: after reactivation, the account
stays on the Free plan and the usual rules apply (frozen key, hence `401 unauthorized`, or
`403 plan_required`).

## Multi-tenant isolation

Each key only acts on the resources of the account that issued it. A resource that does not
exist OR belongs to another account **always** returns `404 not_found` — never `403`: the
API never confirms that a resource of another account exists.

## Rotation and revocation

**Rotating** a key, from the dashboard, generates a new secret and invalidates the old one at
the same instant: a key never has two valid secrets at the same time. Only the secret changes:
the key keeps its identifier (the part before the `.`), its name and its rights; until the tool
that uses it receives the new value, its calls are refused with `401 unauthorized`. **Revoking**
a key makes it unusable immediately and permanently (`401 unauthorized`): it can no longer be
reactivated or rotated. QR Suisse can also revoke all the keys of an account, active or frozen,
at the request of one of the account's administrators or if a key may have been exposed or used
without the account holder's consent; the account's administrators are informed by email.
Neither rotation nor revocation changes or deletes any QR code.

## Keeping the key secret

- Treat the full key as a password: store it in a secrets manager or an environment variable,
  never in versioned code, a shared file, a ticket or a screenshot.
- Never paste the key into a shared or stored conversation, including with an AI assistant. An
  agent receives the key through its configuration (environment variable, secrets manager) and
  never asks the user to type it into the conversation.
- Send it only in the `Authorization` header, over HTTPS, to `https://app.qrsuisse.ch/api/v1` —
  never in a URL. Do not log this header.
- Give a key only the rights it needs: for example `QR_READ` and `STATS_READ` (the "AI assistant
  agent" preset in the dashboard) for an agent that only looks things up. With the Entreprise
  plan, use one key per tool, so that you can revoke one without cutting off the others.
- If the key may have been exposed, rotate or revoke it immediately from the dashboard; if the
  account is on the Free plan, write to contact@qrsuisse.ch from the email address of one of the
  account's administrators. An agent that suspects its key may have been exposed reports it to
  the account holder.

## Rate limiting

Three independent buckets:
- **Generic** — 30 requests / 60 seconds per API key, applied to **every** authenticated
  request on the 8 endpoints.
- **Creation** — 8 requests / 60 seconds per API key, applied **on top of** the generic
  bucket, only on `POST /qr-codes`.
- **PNG image** — 10 renders / 60 seconds **per account** (shared by all the keys of the
  account), applied **on top of** the generic bucket, only on `GET /qr-codes/{id}/image` in
  PNG format. Neither the SVG format nor a `304` response counts towards it.

Exceeding a limit returns `429 rate_limited` with a `Retry-After` header (seconds). The
buckets of a key are counted as soon as the key and its secret are recognised, including
when the request is then refused (`403 account_disabled`, `plan_required` or `forbidden`):
once a bucket is exhausted, `429 rate_limited` is returned, before these refusals.

Requests refused with `401 unauthorized` are additionally limited by IP address:
60 refusals / 60 seconds per IPv4 address or per IPv6 /64 network, and 300 refusals /
60 seconds per IPv6 /48 network. Beyond that, the API returns `429 rate_limited` (with
`Retry-After`) without checking the key, even a valid one, until the end of the window.

## Common error format

Every error of this API returns the same shape `{ "error": "<code>", "message": "<text>",
...possible details }`. 14 stable codes: an existing code never changes; a minor version may
add new ones, only for a new and optional feature (see "Version history"). The codes:
`unauthorized` (401),
`plan_required` (403), `forbidden` (403), `account_disabled` (403), `https_required` (403),
`not_found` (404), `method_not_allowed` (405), `validation_error` (400), `quota_exceeded`
(429), `rate_limited` (429), and for `POST /qr-codes` with the `Idempotency-Key` header:
`idempotency_key_mismatch` (422), `idempotency_in_progress` (409), `resource_deleted` (410),
and for `POST /qr-codes` and `PATCH /qr-codes/{id}`: `payload_too_large` (413), request body
larger than 64 KiB.
The `message` text is a reading aid, in French as in the examples of this specification, and
it may change without notice. Base your code on the `error` code, never on `message`.
A URL path containing an invalid `%` sequence (e.g. `%zz`) is refused before any
authentication with `400 validation_error`. An unknown path under `/api/v1/` returns
`404 not_found`. An HTTP method that an existing endpoint does not support returns
`405 method_not_allowed`, with an `Allow` header listing the accepted methods. These two
responses are the same with or without an API key. A request over HTTP receives
`403 https_required` before all these checks (see "Authentication").

## See also

- [OpenAPI specification (JSON)](https://www.qrsuisse.ch/openapi.json)
- [OpenAPI specification (YAML)](https://www.qrsuisse.ch/openapi.yaml)
- [API documentation](https://www.qrsuisse.ch/en/api-documentation)
- [Manage your API keys (help)](https://www.qrsuisse.ch/en/help/api-keys)
- [API catalog (RFC 9727)](https://www.qrsuisse.ch/.well-known/api-catalog)
- [llms.txt](https://www.qrsuisse.ch/llms.txt)
