API · v1

API documentation

From your first request to statistics: examples ready to adapt.

Download OpenAPI specification

Authentication

Create a key in your developer space. Send it only from your server in the Authorization header. Never embed it in a public page.

Authorization: Bearer <TOKEN>
Accept: application/json
Content-Type: application/json

60 requests per minute, shared across the subscription owner’s keys, including their companies. On a 429 response, respect Retry-After. The key keeps its workspace and its creator’s permissions.

Existing qr:* keys remain compatible. New permissions separate each operation. No key can manage passwords or payments.

Add codes:read to receive full content in a mutation response. Otherwise, only the operation confirmation is returned. Statistics never include private content.

Every creation requires a stable Idempotency-Key. An identical retry returns the same code. Different content with the same key returns 409.

Available operations

HTTPOperationPermission
POST /api/v1/qr-imagesQR for your emails and applicationsimages:render
GET /api/v1/qr-codes/{id}/pngPNG and JPEG imagescodes:export
GET /api/v1/qr-codes/{id}/jpegPNG and JPEG imagescodes:export
POST /api/v1/print-sheetsPDF sheets through the APIprints:export
GET /api/v1/qr-codesList my codescodes:read
POST /api/v1/qr-codesCreate a codecodes:create
GET /api/v1/qr-codes/{id}Read a codecodes:read
PATCH /api/v1/qr-codes/{id}Change content and schedulescodes:update
GET /api/v1/qr-codes/{id}/svgExport the QR or its SVG compositioncodes:export
GET /api/v1/qr-codes/{id}/statisticsRead its countersstatistics:read
PATCH /api/v1/qr-codes/{id}/stateDisable or reactivate linkscodes:state
DELETE /api/v1/qr-codes/{id}Disable its linkcodes:state

Changing disabled also requires codes:state in the general PATCH. Only dynamic links can be disabled or reactivated.

DELETE disables the link; it does not trash the code or free a slot.

Create static or editable QR codes. Only editable content can be updated; DELETE disables an editable link.

History through the API

History returns identifiers, dates, sources and change categories without previous private contents. Permissions are separate and are not added to existing keys.

Use meta.revision from the list to restore. An intervening change returns 409: read history again before retrying. Access, dates and quotas remain enforced.

HTTPPermission
GET /qr-codes/{id}/historyhistory:read
GET /qr-codes/{id}/history/{version}history:read + codes:read
POST /qr-codes/{id}/history/{version}/restorehistory:restore
cURL
curl --fail-with-body "https://preprod.qrissimo.fr/api/v1/qr-codes/$QR_ID/history" \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -H 'Accept: application/json'
curl --fail-with-body -X POST "https://preprod.qrissimo.fr/api/v1/qr-codes/$QR_ID/history/$VERSION_ID/restore" \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" \
  -H 'Content-Type: application/json' \
  -d "{\"revision\":\"$REVISION\"}"

PNG and JPEG images

Keep the drawing, caption and arrow, or choose the QR only. Size refers to the longest side.

Requires codes:export. size: 512, 1024 or 2048 (default 1024), longest side. composition=1 keeps the drawing, caption and arrow; otherwise QR only. transparent=1: complete PNG only, QR margin always opaque. A download is counted after success and rechecking access. Shared limits: 10/minute/user, 20/minute/account holder, 30/minute/IP. JSON errors.

cURL
curl --fail-with-body 'https://preprod.qrissimo.fr/api/v1/qr-codes/42/png?size=1024&composition=1&transparent=1' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -o qr.png
curl --fail-with-body 'https://preprod.qrissimo.fr/api/v1/qr-codes/42/jpeg?size=2048&composition=1' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -o qr.jpg

Caption and arrow placement

The caption and arrow stay outside the illustration and the QR quiet zone. Text adjusts to avoid splitting ordinary words.

caption_position: auto | top | bottom | left | right · caption_offset: 0..100 · caption_scale: 80..140 · caption_align: left | center | right

JSON
{"destination":"https://example.com/event","composition":true,"template":"plain","caption":"Scan me","arrow":"curve","caption_position":"bottom","caption_offset":50,"caption_scale":120,"caption_align":"center","size":2048}

Rotation and layers

Move each element independently within the callout. The QR stays protected.

caption_font: sans | serif | mono | caveat | patrick_hand · caption_rotation: -180..180 · caption_dx / caption_dy / arrow_dx / arrow_dy / logo_dx / logo_dy: -100..100

arrow_scale: 80..140 · logo_scale: 50..150 · logo_mode: header | callout | center · caption_color / arrow_color: #RRGGBB

Layer order: logo-arrow-text | logo-text-arrow | arrow-logo-text | arrow-text-logo | text-logo-arrow | text-arrow-logo

JSON
{"composition":true,"destination":"https://example.com/event","caption":"Scan me","caption_font":"caveat","caption_rotation":-15,"caption_position":"bottom","arrow":"curve","caption_dx":-30,"arrow_dx":40,"composition_layers":"logo-arrow-text","size":2048}

QR colours

Markers remain solid. Both colours are checked for contrast before export.

color_mode: solid | linear | radial · gradient_color: #RRGGBB · gradient_angle: 0..359

PNG or JPEG, up to 2 MiB. Choose the header, callout or QR centre. Central placement preserves markers and passes a reading check.

logo_mode: center · logo_dx: 0 · logo_dy: 0 · logo_scale: 50..150

JSON
{"destination":"https://example.com/catalogue","color_mode":"linear","foreground":"#182B2A","background":"#FFFFFF","gradient_color":"#164F72","gradient_angle":135,"size":1024}

QR for your emails and applications

POST /qr-images renders an image without creating a library QR. Requires images:render. The HTTPS URL is encoded without being fetched or stored. Only the format, size, actor and successful render count are recorded; no scan statistics or editable redirect. Embed the received bytes as a CID image in your email.

Use composition=1 to choose a template, caption, arrow or transparent background. template, caption, arrow · caption ≤ 30

Transparency applies only to complete PNG designs. The QR background and quiet zone stay opaque to preserve readability.

cURL
curl --fail-with-body -X POST 'https://preprod.qrissimo.fr/api/v1/qr-images' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -H 'Content-Type: application/json' \
  -d '{"destination":"https://example.com/event","format":"png","size":512,"modules":"rounded","finders":"rounded"}' \
  -o email-qr.png

curl --fail-with-body -X POST 'https://preprod.qrissimo.fr/api/v1/qr-images' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -H 'Content-Type: application/json' \
  -d '{"destination":"https://example.com/event","composition":true,"template":"vinyl","caption":"Scan me","arrow":"curve","size":1024,"transparent":true}' \
  -o event-design.png
JavaScript · Node.js · Nodemailer
const response = await fetch(base + '/qr-images', {
  method: 'POST', headers,
  body: JSON.stringify({ destination: 'https://example.com/event', size: 512 }),
});
if (!response.ok) throw new Error('QRissimo ' + response.status + ': ' + await response.text());
const qrImage = Buffer.from(await response.arrayBuffer());
const email = {
  html: '<p>Your event</p><img src="cid:event-qr" width="176" height="176" alt="Event QR">',
  attachments: [{ filename: 'event-qr.png', content: qrImage,
    contentType: 'image/png', cid: 'event-qr' }],
};
// Add this content to your server-side Nodemailer message.
Python · requests · EmailMessage
from email.message import EmailMessage

response = session.post(f'{base}/qr-images', timeout=15, json={
    'destination': 'https://example.com/event', 'size': 512,
})
response.raise_for_status()
message = EmailMessage()
message.set_content('Your event: https://example.com/event')
message.add_alternative('<img src="cid:event-qr" width="176" height="176" alt="Event QR">', subtype='html')
message.get_payload()[-1].add_related(response.content,
    maintype='image', subtype='png', cid='<event-qr>')
# Send message through your application's own SMTP configuration.

Available contents

Eight static or editable content types, plus imported raw content for static QR only. Fields are described in OpenAPI. Types stay permanent. Wi-Fi passwords and imported content are never returned in JSON.

cURL · Wi-Fi
curl -X POST 'https://preprod.qrissimo.fr/api/v1/qr-codes' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -H 'Idempotency-Key: guest-wifi-1' \
  -d '{"label":"Guest network","content_type":"wifi","kind":"static","wifi_ssid":"Guest","wifi_security":"nopass"}'
JavaScript · vCard
const response = await fetch(base + '/qr-codes', {
  method: 'POST', headers: { ...headers, 'Idempotency-Key': 'contact-1' },
  body: JSON.stringify({ label: 'Contact', content_type: 'vcard',
    contact_first: 'Camille', contact_phone: '+33612345678' }),
});
if (!response.ok) throw new Error('QRissimo ' + response.status + ': ' + await response.text());
const { data } = await response.json();
console.log(data.short_url);
Python · SMS
response = session.post(f'{base}/qr-codes', timeout=15,
    headers={'Idempotency-Key': 'sms-1'},
    json={'label': 'Message', 'content_type': 'sms', 'kind': 'static',
          'phone': '+33612345678', 'message': 'Bonjour'})
response.raise_for_status()
print(response.json()['data']['id'])

API customization

Change only the desired fields. The encoded content is preserved; the server adapts the matrix to the design. Colors are checked before saving.

GET /api/v1/catalog?locale=fr · catalog:read

PATCH /api/v1/qr-codes/{id}/appearance · codes:style

cURL
curl 'https://preprod.qrissimo.fr/api/v1/catalog?locale=fr' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -H 'Accept: application/json'

curl -X PATCH 'https://preprod.qrissimo.fr/api/v1/qr-codes/42/appearance' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"template":"vinyl","caption":"Scannez-moi !","arrow":"curve","modules":"rounded","finders":"circle","foreground":"#182B2A","background":"#FFFFFF"}'
JavaScript
const response = await fetch(base + '/qr-codes/42/appearance', {
  method: 'PATCH', headers,
  body: JSON.stringify({ template: 'car', arrow: 'curve' }),
});
if (!response.ok) throw new Error('QRissimo ' + response.status + ': ' + await response.text());
const { data } = await response.json();
Python
response = session.patch(f'{base}/qr-codes/42/appearance', timeout=15,
    json={'template': 'vinyl', 'arrow': 'wave'})
response.raise_for_status()
print(response.json()['data']['style'])

Compositions and silhouettes

Choose a template identifier when creating a code. caption accepts up to 30 characters; arrow accepts none, curve, wave or loop. Export returns the QR alone by default. Add composition=1 for the saved silhouette, caption and arrow.

Graphic designs · 24
  • Essential · plain
  • Signature · card
  • Badge · round
  • Seal · seal
  • Viewfinder · corners
  • Bracket · bracket
  • Bubble · bubble
  • Tag · tag
  • Ticket · ticket
  • Band · band
  • Snapshot · polaroid
  • Heart · heart
  • Arch · arch
  • Fold · fold
  • Capsule · capsule
  • Hexagon · hexagon
  • Orbit · orbit
  • Echo · ripple
  • Editorial · editorial
  • Rails · rails
  • Ribbon · ribbon
  • Pennant · pennant
  • Markers · grid
  • Rosette · rosette
Transport · 8
  • Car · car
  • Van · van
  • Bus · bus
  • Cargo bike · bicycle
  • Rocket · rocket
  • Airplane · plane
  • Boat · boat
  • Train · train
Nature and animals · 10
  • Cat · cat
  • Dog · dog
  • Panda · bear
  • Fox · fox
  • Rabbit · rabbit
  • Owl · owl
  • Butterfly · butterfly
  • Flower · flower
  • Leaf · leaf
  • Tree · tree
Everyday objects · 10
  • House · house
  • Camera · camera
  • Phone · phone
  • Laptop · laptop
  • Cup · cup
  • Book · book
  • Light bulb · bulb
  • Gift · gift
  • Bottle · bottle
  • Padlock · lock
Leisure and creativity · 8
  • Game controller · gamepad
  • Football · football
  • Basketball · basketball
  • Music notes · music
  • Vinyl record · vinyl
  • Clapperboard · cinema
  • Palette · palette
  • Crown · crown

Complete examples

cURL
curl -X POST 'https://preprod.qrissimo.fr/api/v1/qr-codes' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: panneau-123' \
  -d '{"label":"Panneau 123","destination":"https://example.com/panneau/123"}'

curl 'https://preprod.qrissimo.fr/api/v1/qr-codes/42/svg' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -o panneau.svg

curl -X POST 'https://preprod.qrissimo.fr/api/v1/qr-codes' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: voiture-456' \
  -d '{"label":"Voiture","destination":"https://example.com/auto","template":"car","caption":"Scannez-moi !","arrow":"curve"}'

curl 'https://preprod.qrissimo.fr/api/v1/qr-codes/43/svg?composition=1' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -o voiture.svg

curl -X PATCH 'https://preprod.qrissimo.fr/api/v1/qr-codes/42' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"destination":"https://example.com/panneau/456"}'

curl -X PATCH 'https://preprod.qrissimo.fr/api/v1/qr-codes/42/state' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"disabled":false}'
JavaScript · Node.js
const base = 'https://preprod.qrissimo.fr/api/v1';
const headers = {
  Authorization: `Bearer ${process.env.QRISSIMO_TOKEN}`,
  Accept: 'application/json',
  'Content-Type': 'application/json',
};
const response = await fetch(`${base}/qr-codes`, {
  method: 'POST',
  headers: { ...headers, 'Idempotency-Key': 'panneau-123' },
  body: JSON.stringify({ label: 'Panneau 123', destination: 'https://example.com/panneau/123' }),
});
if (!response.ok) throw new Error(`QRissimo ${response.status}: ${await response.text()}`);
const { data } = await response.json();
const stats = await fetch(`${base}/qr-codes/${data.id}/statistics`, { headers });
if (!stats.ok) throw new Error(`QRissimo ${stats.status}`);
console.log(await stats.json());
Python · requests
import os
import requests

base = 'https://preprod.qrissimo.fr/api/v1'
session = requests.Session()
session.headers.update({
    'Authorization': f'Bearer {os.environ["QRISSIMO_TOKEN"]}',
    'Accept': 'application/json',
})
response = session.post(f'{base}/qr-codes', timeout=15,
    headers={'Idempotency-Key': 'panneau-123'},
    json={'label': 'Panneau 123', 'destination': 'https://example.com/panneau/123'})
response.raise_for_status()
code = response.json()['data']
svg = session.get(f'{base}/qr-codes/{code["id"]}/svg', timeout=15)
svg.raise_for_status()
with open('panneau.svg', 'wb') as output:
    output.write(svg.content)

Campaigns, periods and scheduling

Statistics accept from and to (YYYY-MM-DD), timezone, campaign_id, code_id and q. The period includes both dates and at most 366 days. Schedule values are local times YYYY-MM-DDTHH:mm in the supplied time zone; null removes a limit. Responses use UTC. Missing or repeated clock-change times are rejected.

An opening is a visit to a QRissimo link, from a scan or a click. Repeat and automated visits may be counted; these are not unique visitors. Static QR codes cannot measure openings. History is kept for QR codes in the trash.

HTTPOperationPermission
GET /api/v1/statisticsStatisticsstatistics:read
GET /api/v1/statistics.csvExport CSVstatistics:read
GET /api/v1/campaignsCampaignscampaigns:read
POST /api/v1/campaignsCreate a campaigncampaigns:write
GET /api/v1/campaigns/{campaign}Choose QR codescampaigns:read
PATCH /api/v1/campaigns/{campaign}Save namecampaigns:write
POST /api/v1/campaigns/{campaign}/codesChoose QR codescampaigns:write
DELETE /api/v1/campaigns/{campaign}Delete this campaigncampaigns:write
cURL
curl 'https://preprod.qrissimo.fr/api/v1/statistics?from=2026-10-01&to=2026-10-31&timezone=Europe%2FParis' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -H 'Accept: application/json'

curl -X POST 'https://preprod.qrissimo.fr/api/v1/campaigns' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"Collection automne"}'

curl -X POST 'https://preprod.qrissimo.fr/api/v1/campaigns/7/codes' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -H 'Content-Type: application/json' \
  -d '{"action":"add","codes":[42,43]}'

curl -X PATCH 'https://preprod.qrissimo.fr/api/v1/qr-codes/42' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -H 'Content-Type: application/json' \
  -d '{"starts_at":"2026-11-01T09:00","ends_at":"2026-11-30T18:00","timezone":"Europe/Paris","fallback_url":"https://example.com/fin"}'
JavaScript · Node.js
const params = new URLSearchParams({
  from: '2026-10-01', to: '2026-10-31',
  timezone: 'Europe/Paris', campaign_id: '7',
});
const response = await fetch(base + '/statistics?' + params, { headers });
if (!response.ok) throw new Error('QRissimo ' + response.status + ': ' + await response.text());
const { data } = await response.json();
console.log(data.summary, data.daily);
Python · requests
response = session.get(f'{base}/statistics', timeout=15, params={
    'from': '2026-10-01', 'to': '2026-10-31',
    'timezone': 'Europe/Paris', 'campaign_id': 7,
})
response.raise_for_status()
report = response.json()['data']
print(report['summary'], report['daily'])

Bulk creation through the API

Import a CSV/XLSX file or a JSON list of rows. No QR codes are created before confirmation. Limits: 100 rows, 32 columns, 1 MB, five active drafts per workspace; drafts expire after 24 hours.

Batch and API access are required. Explicit scopes batches:read, batches:write and batches:export; legacy keys do not receive them automatically. Confirmation also requires codes:create; ZIP export also requires codes:export. Write-only keys receive a summary without row contents.

Idempotency-Key is required for import. Identical retries return the same batch; different data returns 409. Column indices start at zero. Use revision=null for the first mapping, then the latest revision. Confirm exclude_invalid=true only after reviewing errors. Repeated confirmation never duplicates or restores trashed QR codes.

HTTPPermission
GET /api/v1/batchesbatches:read
POST /api/v1/batchesbatches:write
GET /api/v1/batches/{id}batches:read
PATCH /api/v1/batches/{id}/mappingbatches:write
POST /api/v1/batches/{id}/confirmbatches:write + codes:create
DELETE /api/v1/batches/{id}batches:write
GET /api/v1/batches/{id}/manifest.csvbatches:read
GET /api/v1/batches/{id}/svg.zipbatches:export + codes:export
cURL · JSON
curl 'https://preprod.qrissimo.fr/api/v1/batches' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -H 'Idempotency-Key: menus-2026-10' \
  -d '{"kind":"static","template":"vinyl","caption":"Scannez-moi","arrow":"curve","rows":[{"label":"Menu","destination":"https://example.com/menu","reference":"00042"}]}'

curl "https://preprod.qrissimo.fr/api/v1/batches/$BATCH_ID/confirm" \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d "{\"revision\":\"$REVISION\",\"exclude_invalid\":false}"

curl "https://preprod.qrissimo.fr/api/v1/batches/$BATCH_ID/svg.zip" \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" \
  --fail-with-body -o lot.zip
cURL · CSV/XLSX
curl 'https://preprod.qrissimo.fr/api/v1/batches' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -H 'Accept: application/json' \
  -H 'Idempotency-Key: fichier-menus-2026-10' -F 'file=@menus.csv'

curl -X PATCH "https://preprod.qrissimo.fr/api/v1/batches/$BATCH_ID/mapping" \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"revision":null,"mapping":{"label":0,"destination":1,"reference":2},"kind":"static","template":"car"}'
JavaScript · Node.js
const prepared = await fetch(base + '/batches', {
  method: 'POST', headers: { ...headers, 'Idempotency-Key': 'menus-2026-10' },
  body: JSON.stringify({ kind: 'static', template: 'vinyl', rows: [
    { label: 'Menu', destination: 'https://example.com/menu', reference: '00042' },
  ] }),
});
if (!prepared.ok) throw new Error('QRissimo ' + prepared.status + ': ' + await prepared.text());
const { data: batch } = await prepared.json();
if (batch.summary.invalid) throw new Error('Review invalid rows before confirming');
const confirmed = await fetch(base + '/batches/' + batch.id + '/confirm', {
  method: 'POST', headers, body: JSON.stringify({ revision: batch.revision }),
});
if (!confirmed.ok) throw new Error('QRissimo ' + confirmed.status + ': ' + await confirmed.text());
const result = (await confirmed.json()).data;
console.log(result.summary.created);
Python · requests
prepared = session.post(f'{base}/batches', timeout=30,
    headers={'Idempotency-Key': 'menus-2026-10'}, json={
      'kind': 'static', 'template': 'vinyl', 'rows': [
        {'label': 'Menu', 'destination': 'https://example.com/menu', 'reference': '00042'}]})
prepared.raise_for_status()
batch = prepared.json()['data']
if batch['summary']['invalid']:
    raise ValueError('Review invalid rows before confirming')
confirmed = session.post(f'{base}/batches/{batch["id"]}/confirm', timeout=60,
    json={'revision': batch['revision']})
confirmed.raise_for_status()
print(confirmed.json()['data']['summary'])

The ZIP contains complete SVGs of QR codes still in the collection; each exported QR adds a download. The CSV summary does not count as a QR download. Use row code IDs for individual PNG/JPEG exports or PDF sheets. Respect Retry-After on 429.

PDF sheets through the API

Generate an A4 sheet with 2, 6, 12 or 24 QR codes per page, using the saved designs and arrows. Select up to 48 creations from the same workspace. Names are optional; identifiers determine ascending order. Each exported QR is counted.

The prints:export permission is required. Legacy qr:export keys do not automatically receive this new permission. Foreign selections, trashed codes and QR codes too dense for the chosen layout are rejected.

cURL
curl -X POST 'https://preprod.qrissimo.fr/api/v1/print-sheets' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" \
  -H 'Content-Type: application/json' -H 'Accept: application/pdf' \
  -d '{"codes":[42,43],"layout":12,"labels":true}' \
  --fail-with-body -o planche.pdf
JavaScript · Node.js
import { writeFile } from 'node:fs/promises';
const sheet = await fetch(base + '/print-sheets', {
  method: 'POST', headers: { ...headers, Accept: 'application/pdf' },
  body: JSON.stringify({ codes: [42, 43], layout: 12, labels: true }),
});
if (!sheet.ok) throw new Error('QRissimo ' + sheet.status + ': ' + await sheet.text());
await writeFile('planche.pdf', Buffer.from(await sheet.arrayBuffer()));
Python · requests
sheet = session.post(f'{base}/print-sheets', timeout=60,
    headers={'Accept': 'application/pdf'},
    json={'codes': [42, 43], 'layout': 12, 'labels': True})
sheet.raise_for_status()
with open('planche.pdf', 'wb') as output:
    output.write(sheet.content)

Errors and limits

200 · 201 · 204
401 · 403 · 404 · 409 · 410 · 422 · 429

{ "message": "…", "errors": { "destination": ["…"] } }
Retry-After: 60

Reusable styles

Explicit kits:read and kits:write permissions. A write-only key receives the identifier and revision without the style. Copy the seven style fields into your creations or batches.

Per workspace: 100 active styles, 250 retained including archives.

HTTPPermission
GET /style-kitskits:read
GET /style-kits/{id}kits:read
POST /style-kitskits:write
PUT /style-kits/{id}kits:write
POST /style-kits/{id}/archivekits:write
POST /style-kits/{id}/restorekits:write
cURL
curl --fail-with-body -X POST 'https://preprod.qrissimo.fr/api/v1/style-kits' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $REQUEST_UUID" \
  -d '{"name":"Studio","modules":"rounded","finders":"rounded","foreground":"#182B2A","background":"#FFFFFF","template":"card","caption":"Scan","arrow":"curve"}'
curl --fail-with-body 'https://preprod.qrissimo.fr/api/v1/style-kits?archived=1' \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -H 'Accept: application/json'

Hosted pages through the API

Create a dynamic QR with content_type=page, page_type and codes:create + pages:write permissions. page_id identifies the draft. Save each language, check the private preview, then publish explicitly. Exactly one QR slot is used.

pages:read reads the draft and preview token; pages:write edits the draft; pages:publish publishes or withdraws the page. Write-only keys never receive private contents.

40 blocks per language, 128 KiB per request and draft, 30 retained versions. Plain text; HTTP(S) links without credentials. Readable accent on white. Other languages remain private until publication.

HTTPPermission
GET /pagespages:read
GET /pages/{id}pages:read
PUT /pages/{id}pages:write
GET /pages/{id}/preview?revision={revision}&lang=frpages:read
POST /pages/{id}/publishpages:publish
POST /pages/{id}/unpublishpages:publish
DELETE /pages/{id}/locales/{locale}pages:write
GET /pages/{id}/historypages:read
POST /pages/{id}/history/{version}/restorepages:write
cURL
curl --fail-with-body -X POST 'https://preprod.qrissimo.fr/api/v1/qr-codes' \
 -H "Authorization: Bearer $QRISSIMO_TOKEN" -H 'Content-Type: application/json' \
 -H 'Idempotency-Key: page-menu-2026-10' \
 -d '{"label":"Menu","kind":"dynamic","content_type":"page","page_type":"menu","modules":"rounded","finders":"rounded","foreground":"#182B2A","background":"#FFFFFF"}'

curl --fail-with-body -X PUT "https://preprod.qrissimo.fr/api/v1/pages/$PAGE_ID" \
 -H "Authorization: Bearer $QRISSIMO_TOKEN" -H 'Content-Type: application/json' \
 --data-binary @draft.json

curl --fail-with-body "https://preprod.qrissimo.fr/api/v1/pages/$PAGE_ID/preview?revision=$REVISION&lang=fr" \
 -H "Authorization: Bearer $QRISSIMO_TOKEN" -o private-preview.html

curl --fail-with-body -X POST "https://preprod.qrissimo.fr/api/v1/pages/$PAGE_ID/publish" \
 -H "Authorization: Bearer $QRISSIMO_TOKEN" -H 'Content-Type: application/json' \
 -d "{\"revision\":\"$REVISION\",\"preview_token\":\"$PREVIEW_TOKEN\",\"confirmed\":true}"
JSON · draft.json
{
 "revision": "CURRENT_REVISION_UUID",
 "locale": "fr", "default_locale": "fr",
 "brand_name": "Le Jardin", "accent": "#182B2A",
 "title": "Notre menu", "intro": "Une cuisine de saison.",
 "blocks": [{"type":"menu_item", "title":"Le plat du jour",
   "body":"Description du restaurant", "price":"Prix fourni par le restaurant",
   "allergens":"Allergènes renseignés par le restaurant"}]
}
JavaScript · Node.js
const read = await fetch(base + '/pages/' + pageId, { headers });
if (!read.ok) throw new Error('QRissimo ' + read.status);
const current = (await read.json()).data;
const saved = await fetch(base + '/pages/' + pageId, {
 method: 'PUT', headers,
 body: JSON.stringify({ revision: current.revision, locale: 'en',
  default_locale: current.draft.default_locale,
  brand_name: current.draft.brand.name, accent: current.draft.brand.accent,
  title: 'Welcome', intro: 'Your company content.', blocks: [] }),
});
if (!saved.ok) throw new Error('QRissimo ' + saved.status + ': ' + await saved.text());
const next = (await saved.json()).data;
// Explicitly review the saved draft before publishing all variants.
console.log(base + '/pages/' + pageId + '/preview?revision=' + next.revision);
Python · requests
response = session.get(f'{base}/pages/{page_id}', timeout=15)
response.raise_for_status()
current = response.json()['data']
# Review every draft variant before explicit publication.
publication = session.post(f'{base}/pages/{page_id}/publish', timeout=30,
 json={'revision': current['revision'],
       'preview_token': current['preview_token'], 'confirmed': True})
publication.raise_for_status()

A public click is an observed action, not a sale. Statistics expose page_clicks, page_bot_actions and period_page_clicks; detected bot clicks are excluded from page_clicks.

Photos and documents

Upload a file, then choose it in a draft section. It is public only when the page using it is published. 20 files and 100 MiB per page; 10 MiB per file, images up to 12 megapixels.

PDFs are reconstructed: links, annotations, forms and signatures are not retained. Up to 50 pages. Some protected or compressed PDFs are unsupported. Add a readable summary to the page.

Upload the file to this page library first.

HTTPPermission
GET /pages/{id}/mediapages:read
GET /pages/{id}/media/{asset}pages:read
POST /pages/{id}/mediapages:write
DELETE /pages/{id}/media/{asset}pages:write
cURL · multipart
curl --fail-with-body -X POST "https://preprod.qrissimo.fr/api/v1/pages/$PAGE_ID/media" \
 -H "Authorization: Bearer $QRISSIMO_TOKEN" -H 'Accept: application/json' \
 -F 'file=@guide.pdf' -F 'name=Guide' \
 -F "revision=$REVISION" -F "request_id=$REQUEST_UUID"

curl --fail-with-body "https://preprod.qrissimo.fr/api/v1/pages/$PAGE_ID/media/$ASSET_ID" \
 -H "Authorization: Bearer $QRISSIMO_TOKEN" -o guide.pdf
JSON · PDF document
{"type":"document", "asset_id":"ASSET_UUID", "title":"Guide",
 "body":"An accessible summary for this document."}

{"type":"image", "asset_id":"ASSET_UUID", "title":"",
 "alt":"A useful description of the photo.", "body":"Optional caption."}

0 observed public document downloads. These requests do not necessarily represent distinct visitors. page_file_downloads · period_page_file_downloads

Feedback & requests

Read and manage the inbox with dedicated permissions. Historical keys do not receive these permissions.

Responses are automatically deleted after 180 days. They are private; no newsletter subscription.

HTTPPermission
GET /feedbackfeedback:read
GET /feedback/{entry}feedback:read
PATCH /feedback/{entry}feedback:write
DELETE /feedback/{entry}feedback:write
GET /feedback/preferencesfeedback:notify
PUT /feedback/preferencesfeedback:notify
POST /feedback/{entry}/repliesfeedback:reply
{
    "type": "form",
    "title": "Response",
    "purpose": "feedback",
    "scale": "half",
    "contact": "optional",
    "privacy": "Explain who receives responses, why, and how people can exercise their rights. Registration is not marketing consent."
}
curl "$QRISSIMO_URL/api/v1/feedback?status=new" \
  -H "Authorization: Bearer $QRISSIMO_API_KEY"

curl -X PATCH "$QRISSIMO_URL/api/v1/feedback/$ENTRY_ID" \
  -H "Authorization: Bearer $QRISSIMO_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"revision":"REVISION_UUID","status":"resolved"}'
const inbox = await fetch(`${base}/api/v1/feedback?status=new`, {
  headers: { Authorization: `Bearer ${key}` }
}).then(r => { if (!r.ok) throw new Error(String(r.status)); return r.json(); });

import requests
response = requests.get(base + "/api/v1/feedback", params={"status": "new"},
                        headers={"Authorization": "Bearer " + key}, timeout=15)
response.raise_for_status()
inbox = response.json()

Reply by email

The sender is verified. Your verified account address is used as the reply address. Never share sensitive information with an unverified address.

Replies are limited to 3 per address per day, 50 per workspace and 10 per request. Email limits are separate from your QR code quotas.

An incident prevents us from confirming transmission. No automatic resend, to avoid duplicates. “Accepted” does not guarantee inbox delivery.

Summaries are sent at 8 am, on Mondays for weekly summaries. They count new responses since activation. Instant notifications are limited to 20 a day; all responses remain in your inbox.

curl -X POST "$QRISSIMO_URL/api/v1/feedback/$ENTRY_ID/replies" \
  -H "Authorization: Bearer $QRISSIMO_API_KEY" \
  -H "Content-Type: application/json" \
  --data '{"revision":"REVISION_UUID","request_id":"NEW_UUID","message":"Thank you for your feedback.","confirmed":true}'

const result = await fetch(`${base}/api/v1/feedback/${entryId}/replies`, {
  method: 'POST', headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({revision, request_id: crypto.randomUUID(), message: 'Thank you.', confirmed: true})
});
if (!result.ok) throw new Error(String(result.status));
const delivery = await result.json();

import uuid
response = requests.post(base + '/api/v1/feedback/' + entry_id + '/replies',
    headers={'Authorization': 'Bearer ' + key}, timeout=15,
    json={'revision': revision, 'request_id': str(uuid.uuid4()), 'message': 'Thank you.', 'confirmed': True})
response.raise_for_status()

Restaurants: menus, requests and preparation

First create a Menu page. Save a menu, check its preview and explicitly publish it. Every request stays pending until the restaurant confirms. The server checks prices, options, availability and permissions.

Counts cover retained requests: receipt on submission date, confirmation on the manual decision date. A confirmation remains counted after cancellation and is not a payment. Deleted or expired requests are excluded.

HTTPPermission
GET /restaurantsrestaurants:read
GET /restaurants/{page}restaurants:read
PUT /restaurants/{page}restaurants:write
GET /restaurants/{page}/preview?revision=UUIDrestaurants:read
POST /restaurants/{page}/publishrestaurants:publish
POST /restaurants/{page}/withdrawrestaurants:publish
GET /restaurants/{page}/preparation?slot_id=UUIDrestaurants:requests_read
GET /restaurants/{page}/preparation.pdf?slot_id=UUIDrestaurants:export
GET /restaurant-requestsrestaurants:requests_read
POST /restaurant-requestsrestaurants:requests_create
GET /restaurant-requests/{id}restaurants:requests_read
POST /restaurant-requests/{id}/decisionrestaurants:requests_write
DELETE /restaurant-requests/{id}restaurants:requests_write
cURL · The menu
curl -X PUT "$QRISSIMO_URL/api/v1/restaurants/$PAGE_ID" \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -H 'Content-Type: application/json' \
  --data @menu-fr.json

curl "$QRISSIMO_URL/api/v1/restaurants/$PAGE_ID/preview?revision=$REVISION" \
  -H "Authorization: Bearer $QRISSIMO_TOKEN"

curl -X POST "$QRISSIMO_URL/api/v1/restaurants/$PAGE_ID/publish" \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -H 'Content-Type: application/json' \
  --data '{"revision":"REVISION_UUID","preview_token":"TOKEN_FROM_PREVIEW","confirmed":true,"notification_mode":"me"}'
{
    "revision": "CURRENT_REVISION_OR_NULL",
    "locale": "fr",
    "currency": "EUR",
    "timezone": "Europe\/Paris",
    "require_preorder": false,
    "lead_minutes": 60,
    "cancel_hours": 24,
    "max_guests": 8,
    "privacy": "Explain who receives responses, why, and how people can exercise their rights. Registration is not marketing consent.",
    "terms": "The restaurant will confirm your request. Seats and dishes are reserved only after approval.",
    "items": [
        {
            "id": "ITEM_UUID",
            "name": "Dish or set menu",
            "price": "12.50",
            "available": true,
            "max_quantity": 8,
            "stock": 20,
            "options": [
                {
                    "id": "OPTION_UUID",
                    "label": "Option name",
                    "price_cents": 230
                }
            ]
        }
    ],
    "slots": [
        {
            "id": "SLOT_UUID",
            "date": "2026-11-01",
            "time": "19:30",
            "duration_minutes": 120,
            "capacity": 32,
            "enabled": true
        }
    ]
}
JavaScript · Reservation request
const result = await fetch(base + '/restaurant-requests', {
  method: 'POST', headers,
  body: JSON.stringify({
    menu_id: menuId, menu_revision: publishedRevision,
    request_id: crypto.randomUUID(), locale: 'fr',
    selection: [{ id: itemId, quantity: 2, options: [optionId] }],
    slot_id: slotId, guests: 2, name: 'Example', email: 'guest@example.com',
    email_followup: false, confirmed: true,
  }),
});
if (!result.ok) throw new Error(await result.text());
const { data } = await result.json(); // pending; id/status/revision only
Python · Confirm reservation
response = session.post(base + '/restaurant-requests/' + request_id + '/decision',
    timeout=15, json={
        'revision': current_revision, 'action': 'confirm', 'confirmed': True,
    })
response.raise_for_status()
# 409: reload the request and quote. Never force confirmation or change the revision blindly.
# 422: capacity, stock or another field must be reviewed.
cURL · Download preparation PDF
curl --fail-with-body "$QRISSIMO_URL/api/v1/restaurants/$PAGE_ID/preparation.pdf?slot_id=$SLOT_ID" \
  -H "Authorization: Bearer $QRISSIMO_TOKEN" -o restaurant-service.pdf
# Independent scope: restaurants:export. 409 means the preparation changed during rendering.
Tickets and coupons: setup, checks and single use

Create a dynamic page/pass QR, publish with pages:publish, then configure its entitlement. Each ticket uses one QR slot. GET never redeems an entitlement. This module does not trigger payments or ticket delivery.

GET /api/v1/passes?per_page=20
GET /api/v1/codes/{code}/pass
PUT /api/v1/codes/{code}/pass
POST /api/v1/codes/{code}/pass/revoke
POST /api/v1/codes/{code}/pass/redeem

Read the entitlement, then confirm using its revision and page_version, a stable UUID request_id and confirmed=true. An exact retry returns the receipt without another use. A different ID after redemption returns 409; an unavailable entitlement returns 410. Never expose the API key publicly.

curl -X POST "$BASE/codes/$CODE/pass/redeem" \
 -H "Authorization: Bearer $QRISSIMO_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{"revision":"PASS_REVISION_UUID","page_version":"PUBLISHED_VERSION_UUID","request_id":"STABLE_REQUEST_UUID","confirmed":true}'
Product formats and GS1

Create a Data Matrix or GS1 Digital Link QR using your own identifiers.

The identifiers remain yours. Format validation does not prove assignment by GS1. Static codes, without statistics or a new QRissimo redirect.

POST /api/v1/symbols/render    symbols:render
{"type":"datamatrix", "text":"PUMP-001", "format":"svg"}
{"type":"gs1_datamatrix", "gtin":"09520123456788", "lot":"ABC1",
 "serial":"12345", "expiry":"2027-01-31", "format":"png", "size":512}
{"type":"digital_link", "gtin":"09520123456788", "base_url":"https://id.example.org"}

curl --fail-with-body "$BASE/symbols/render" \
 -H "Authorization: Bearer $QRISSIMO_TOKEN" -H "Content-Type: application/json" \
 -d '{"type":"datamatrix","text":"PUMP-001","format":"png"}' -o product.png

Enter an HTTPS origin without path or parameters. The resolver must already exist: this tool does not create a redirect.

Use 1 to 256 printable ASCII characters. Use a complete date between 2000 and 2049.

NFC integration

Export the current address or prepare local writing. The server does not write a tag remotely, and a browser read-back is not a certificate.

GET /api/v1/codes/{code}/nfc                 nfc:read
POST /api/v1/codes/{code}/nfc/prepare        nfc:prepare
GET /api/v1/codes/{code}/nfc/export.ndef     nfc:export
GET /api/v1/codes/{code}/nfc/export.json     nfc:export

curl --fail-with-body "$BASE/codes/$CODE/nfc" \
 -H "Authorization: Bearer $QRISSIMO_TOKEN"

{"revision":"64_HEX_CHARACTERS"}

// Web NFC: explicit user click, HTTPS, compatible Android browser
await new NDEFReader().write({records: [
  {recordType: "url", data: current.url}
]}, {overwrite: false, signal: controller.signal});

The NDEF file contains one URL record. The JSON contains Web NFC data. Tag capacity must also accommodate its memory format.

QR/NFC inventory API

Read objects, configure their references and record interventions. Internal reports require a separate permission. Keep keys on your server.

GET /api/v1/assets?kind=equipment&per_page=20
GET /api/v1/assets.csv?kind=equipment
GET /api/v1/codes/{code}/asset
PUT /api/v1/codes/{code}/asset
GET /api/v1/codes/{code}/asset/checks?page=1
POST /api/v1/codes/{code}/asset/checks

For an intervention, retain the same request_id when retrying. An identical retry returns the original receipt; a different payload or stale revision is refused. Scanning never creates an intervention.

curl --fail-with-body -X POST "$BASE/codes/$CODE/asset/checks" \
 -H "Authorization: Bearer $QRISSIMO_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{"revision":"ASSET_REVISION_UUID","request_id":"STABLE_REQUEST_UUID","kind":"maintenance","note":"Filter checked","confirmed":true}'

// Node.js · assets:checks
const receipt = await fetch(`${base}/codes/${code}/asset/checks`, {
  method: "POST",
  headers: {Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json"},
  body: JSON.stringify({revision, request_id: requestId, kind: "inspection",
    note: "Checked connections", confirmed: true, next_due: "2040-01-01T12:00:00Z"})
});
if (!receipt.ok) throw new Error(`QRissimo ${receipt.status}`);
const result = await receipt.json();

# Python · assets:history
import requests
response = requests.get(f"{base}/codes/{code}/asset/checks", timeout=15,
    headers={"Authorization": f"Bearer {api_key}"}, params={"page": 1})
response.raise_for_status()
history = response.json()
Price and stock API

Authorized source per product. Integer amounts in currency minor units, integer stock or unknown. A scan does not confirm a sale.

GET /api/v1/commerce/feeds?q=Alba&per_page=20
GET /api/v1/commerce/feeds.csv?q=Alba
GET /api/v1/codes/{code}/commerce
PUT /api/v1/codes/{code}/commerce
POST /api/v1/codes/{code}/commerce
POST /api/v1/commerce/updates

Keep the same request_id and payload when retrying. The last 64 receipts remain replayable for up to seven days. Beyond that, older sequences are refused. A batch is atomic.

curl --fail-with-body -X POST "$BASE/codes/$CODE/commerce" \
 -H "Authorization: Bearer $QRISSIMO_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{"revision":"CURRENT_POLICY_UUID","request_id":"STABLE_REQUEST_UUID","sequence":1,"sku":"SKU-001","price_minor":2090,"currency":"EUR","tax_mode":"included","stock_quantity":7,"observed_at":"2026-10-13T12:00:00Z"}'

// Node.js · commerce:write. Keep the same UUID and payload when retrying.
const response = await fetch(`${base}/commerce/updates`, {
  method: "POST", headers: {Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json"},
  body: JSON.stringify({items: observations})
});
if (!response.ok) throw new Error(`QRissimo ${response.status}`);
const {data: receipts} = await response.json();

# Python · commerce:read
import requests
response = requests.get(f"{base}/codes/{code}/commerce", timeout=15,
    headers={"Authorization": f"Bearer {api_key}"})
response.raise_for_status()
feed = response.json()["data"]
Monitoring API

Periodic HEAD checks, without redirects or downloaded bodies. Three failures to confirm, two successes to recover. Timing depends on the budget and task service.

GET /api/v1/monitoring?q=Catalogue&per_page=20
GET /api/v1/codes/{code}/monitoring
PUT /api/v1/codes/{code}/monitoring
POST /api/v1/codes/{code}/monitoring/check
PUT /api/v1/codes/{code}/monitoring/alerts
POST /api/v1/codes/{code}/monitoring/incidents/{incident}/acknowledge
curl --fail-with-body -X PUT "$BASE/codes/$CODE/monitoring" \
 -H "Authorization: Bearer $QRISSIMO_API_KEY" \
 -H "Content-Type: application/json" \
 -d '{"revision":null,"enabled":true,"interval_minutes":60}'

// Node.js · monitoring:check. Due checks only; never a force refresh.
const response = await fetch(`${base}/codes/${code}/monitoring/check`, {
  method: "POST",
  headers: {Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json"},
  body: JSON.stringify({revision: currentMonitorRevision})
});
if (!response.ok) throw new Error(`QRissimo ${response.status}`);
const {data: {queued}} = await response.json();

# Python · monitoring:read
import requests
response = requests.get(f"{base}/codes/{code}/monitoring", timeout=15,
    headers={"Authorization": f"Bearer {api_key}"})
response.raise_for_status()
for target in response.json()["data"]["targets"]:
    print(target["host"], target["status"], target["checked_at"])

This is a personal choice. Messages go only to your verified account address while you have access to this QR.

At most 20 emails per person per day, 100 per payer and 500 across the service. One alert per incident; uncertain deliveries are never resent automatically.

Batch publishing

Publish several page drafts together after checking each page.

If content or access changes, no page in the batch is published. Your QR links stay the same.

The preview is valid for 30 minutes. Discard it without changing your pages.

GET /api/v1/page-publications                     page_publications:read
POST /api/v1/page-publications                    page_publications:write + pages:read
GET /api/v1/page-publications/{operation}         page_publications:read
GET /api/v1/page-publications/{operation}/pages/{page}/preview
POST /api/v1/page-publications/{operation}/decision
  publish: page_publications:write + pages:publish
  cancel:  page_publications:write
cURL
curl --fail-with-body -X POST "$BASE/page-publications" \
  -H "Authorization: Bearer $QRISSIMO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"request_id":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa","campaign_id":42}'

curl --fail-with-body -X POST "$BASE/page-publications/$OPERATION/decision" \
  -H "Authorization: Bearer $QRISSIMO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"revision\":\"$REVISION\",\"action\":\"publish\",\"confirmed\":true}"
JavaScript
const response = await fetch(`${base}/page-publications`, {
  method: "POST", headers: {Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json"},
  body: JSON.stringify({request_id: crypto.randomUUID(), pages: pageIds})
});
if (!response.ok) throw new Error(`QRissimo ${response.status}`);
const {data: proposal} = await response.json();
// Show proposal.items and each private preview before an explicit decision.
Python
import requests, uuid
response = requests.post(f"{base}/page-publications", timeout=30,
    headers={"Authorization": f"Bearer {api_key}"},
    json={"request_id": str(uuid.uuid4()), "pages": page_ids})
response.raise_for_status()
proposal = response.json()["data"]
# Keep request_id for retries, then show previews before confirming.

From 1 to 50 pages per operation.

Total content exceeds 2 MiB. Prepare smaller batches.

Import pages

Turn a CSV or Excel file into page drafts, then check them before publishing.

Each row creates a new draft in the chosen language. Add sections, photos and other languages in the editor. No page is published automatically.

Up to 50 pages, 1 MiB file. Titles: 120 characters; introductions: 800. Excel formulas are rejected.

POST /api/v1/page-imports                  pages:write + batches:write
GET /api/v1/page-imports/{batch}            pages:read + batches:read
POST /api/v1/page-imports/{batch}/confirm   pages:write + batches:write + codes:create
cURL
curl --fail-with-body -X POST "$BASE/page-imports" \
 -H "Authorization: Bearer $QRISSIMO_API_KEY" -H "Content-Type: application/json" \
 -d '{"request_id":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa","page_type":"guide","locale":"fr","rows":[{"label":"Room guide","title":"Welcome","intro":"Useful information for your stay."}]}'

curl --fail-with-body -X POST "$BASE/page-imports/$BATCH/confirm" \
 -H "Authorization: Bearer $QRISSIMO_API_KEY" -H "Content-Type: application/json" \
 -d "{\"revision\":\"$REVISION\",\"confirmed\":true}"
JavaScript
const prepared = await fetch(`${base}/page-imports`, {
  method: "POST", headers: {Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json"},
  body: JSON.stringify({request_id: crypto.randomUUID(), page_type: "guide", locale: "en",
    rows: [{label: "Room guide", title: "Welcome", intro: "Useful information for your stay."}]})
});
if (!prepared.ok) throw new Error(`QRissimo ${prepared.status}`);
const {data: batch} = await prepared.json();
// Read/check the import before confirming draft creation. Publishing is a separate decision.
Python
import requests, uuid
with open("pages.csv", "rb") as source:
    response = requests.post(f"{base}/page-imports", timeout=30,
        headers={"Authorization": f"Bearer {api_key}"},
        data={"request_id": str(uuid.uuid4()), "page_type": "guide", "locale": "en"},
        files={"file": ("pages.csv", source, "text/csv")})
response.raise_for_status()
batch = response.json()["data"]

The QR codes use their new QRissimo addresses. Complete each page, then prepare a batch preview to publish.

{"request_id":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb","batch_uuid":"IMPORTED_BATCH_UUID"}
POST /api/v1/page-publications
page_publications:write + pages:read + batches:read