Aller au contenu
Créer un compteS'inscrire

L'API d’Inklura Caisse

Afficher la carte d'un établissement sur votre site, lui transmettre des commandes à emporter, relier vos outils à son catalogue, ses ventes et ses commandes, tenir le même stock entre une boutique en ligne et la caisse. Tout est décrit dans une définition OpenAPI et utilisable avec un SDK JavaScript sans dépendance.

Ressources

Démarrer

L'adresse de base est https://caisse.bzh. Les réponses sont en JSON et portent toujours ok : true en cas de succès, sinon false avec un code d'erreur. Montants en euros TTC.

Chaque appel porte un User-Agent qui nomme votre outil, par exemple ma-boutique-sync/1.0. Le pare-feu d’Inklura Caisse écarte les robots d'analyse et les scripts anonymes : les User-Agent par défaut des bibliothèques HTTP de Python (urllib, requests), Go et Java reçoivent un 403 blocked. curl, PHP, Node et les navigateurs conviennent tels quels.

# Python (requests)
requests.get(url, headers={"User-Agent": "ma-boutique-sync/1.0"})

// Go
req.Header.Set("User-Agent", "ma-boutique-sync/1.0")

// Java (java.net.http)
HttpRequest.newBuilder(uri).header("User-Agent", "ma-boutique-sync/1.0")

Pour essayer sans compte, l'établissement de démonstration a pour identifiant comptoir-brest :

curl 'https://caisse.bzh/api/carte?slug=comptoir-brest'

Carte et commandes

Deux appels publics, sans authentification, pour les métiers qui ont une carte en ligne (restaurant, bar). L'identifiant d'un établissement est celui de l'adresse de sa carte, pos.inklura.fr/carte/<slug>.

AppelRôle
GET/api/carte?slug=Identité, horaires, ouverture à l'instant, catégories et articles avec leur id.
POST/api/orderCommande à emporter, payée au retrait. Elle arrive dans « Commandes en ligne » ; l'équipe l'accepte puis la marque prête.
curl -X POST https://caisse.bzh/api/order \
  -H 'content-type: application/json' \
  -d '{"slug":"comptoir-brest","customerName":"Léna","phone":"06 12 34 56 78",
       "pickupAt":"12:30","items":[{"itemId":"en2","qty":2}]}'

La caisse recalcule toujours les prix et le total à partir de sa carte : un montant envoyé est ignoré et une ligne dont l'article est inconnu est retirée. 40 lignes au plus, 20 exemplaires au plus par ligne. Une commande passée en dehors des horaires est acceptée comme précommande.

La carte se lit depuis n'importe quel navigateur. La commande s'envoie depuis votre serveur : l'appel n'est pas ouvert aux navigateurs d'autres sites, et il est limité à 60 commandes par tranche de 10 minutes et par établissement.

import { createClient } from "https://caisse.bzh/sdk/caisse-bzh.mjs";

const caisse = createClient(); // sans clé
const carte = await caisse.getCarte("comptoir-brest");

const commande = await caisse.placeOrder({
  slug: "comptoir-brest",
  customerName: "Léna",
  pickupAt: "12:30",
  items: [{ itemId: carte.items[0].id, qty: 2 }],
});
console.log("Commande n°" + commande.no, commande.total + " €");

Billetterie et boutique sur votre site

Pour les salles de spectacle, les musées et les commerces : montrer sur votre propre site les spectacles, les séances et les articles de la caisse, et envoyer le visiteur réserver ou commander au bon endroit. Le paiement et les coordonnées se font toujours sur la page de la caisse, jamais dans votre site.

Le module de billetterie

Collez ces deux lignes dans une page de votre site. La première étape (spectacle, date, séance, tarifs) s'affiche dans un cadre ; le bouton « Continuer » ouvre la page de la billetterie pour les coordonnées et le paiement. Les attributs data-offer, data-day (AAAA-MM-JJ) et data-session ouvrent le module directement sur un spectacle, un jour ou une représentation.

<div data-caisse-billetterie="theatre-du-kanndi" data-lang="fr"></div>
<script async src="https://caisse.bzh/embed/billetterie.js"></script>

<!-- ouvert directement sur une représentation -->
<div data-caisse-billetterie="theatre-du-kanndi" data-offer="tp_k2m4q8"
     data-day="2026-11-14" data-session="20261114-2030-r8k2q4m"></div>

<!-- un simple lien, sans cadre -->
<div data-caisse-billetterie="theatre-du-kanndi" data-mode="button" data-label="Réserver"></div>

Un lien « Réserver » vers une représentation

L'adresse de la billetterie accepte, après le #, l'offre (o), le jour (d) et la séance (s). Chaque valeur est vérifiée par la page : une offre inconnue, un jour passé ou hors de la période de vente, une séance complète ou suspendue sont ignorés et la page s'ouvre normalement. Avec la séance seule, le jour se déduit de son identifiant.

https://caisse.bzh/billetterie/theatre-du-kanndi#o=tp_k2m4q8
https://caisse.bzh/billetterie/theatre-du-kanndi#o=tp_k2m4q8&d=2026-11-14
https://caisse.bzh/billetterie/theatre-du-kanndi#o=tp_k2m4q8&d=2026-11-14&s=20261114-2030-r8k2q4m
https://caisse.bzh/en/tickets/theatre-du-kanndi#o=tp_k2m4q8

Les identifiants viennent de l'appel public de la billetterie : offerId pour chaque offre, id et day pour chaque séance. Les offres sont mises en cache 30 secondes, les séances 5 secondes.

# les offres en vente en ligne
curl 'https://caisse.bzh/api/webtix/catalog?slug=theatre-du-kanndi&lang=fr'

# les séances d'une offre, du 14 au 20 novembre
curl 'https://caisse.bzh/api/webtix/catalog?slug=theatre-du-kanndi&offer=tp_k2m4q8&from=2026-11-14&days=7'

Le catalogue de la boutique en ligne

Pour un commerce dont la boutique en ligne est ouverte : les articles visibles en ligne, leur prix TTC (le plus bas et le plus haut de leurs déclinaisons), leur photo, ce qu'il en reste et l'adresse de leur page. Sans clé, mis en cache 15 secondes, à lire depuis votre serveur. La commande se passe sur la page de la boutique : retrait en magasin, paiement au retrait (payment: "pickup") ou en ligne quand la boutique le propose (payment: "online").

curl 'https://caisse.bzh/api/boutique/catalog?slug=fournil-kerne'

{ "ok": true, "v": 1, "open": true, "payment": "pickup",
  "shop": { "slug": "fournil-kerne", "name": "Le Fournil Kerné", "url": "https://caisse.bzh/boutique/fournil-kerne", ... },
  "categories": [ { "id": "pains", "name": "Pains", "items": [
    { "id": "baguette", "name": "Baguette tradition", "price": 1.3, "priceMax": 1.3,
      "image": "https://caisse.bzh/demo/alimentaire/baguette.jpg", "soldOut": false, "left": 38,
      "url": "https://caisse.bzh/boutique/fournil-kerne/article?id=baguette", "variants": [] } ] } ] }

Un établissement inconnu, ou dont le métier n'a pas de boutique en ligne, répond 404 unknown_shop. Une boutique fermée répond open: false et aucune catégorie.

Sites qui affichent la billetterie

Aujourd'hui, toute page en https peut afficher la billetterie dans un cadre ; une page en http reçoit un lien vers la billetterie dans un nouvel onglet. Les réglages de la vente en ligne gardent aussi une liste de sites autorisés (champ embedOrigins, par exemple https://www.mon-theatre.fr). Cette liste n'est pas encore appliquée : renseignez-la dès maintenant, elle servira à réserver l'affichage de la billetterie à vos seuls sites.

Clés API

Pour lire et modifier les données d'un établissement depuis vos propres outils (export comptable, tableau de bord, borne, site), créez une clé dans la console : Réglages › Accès API. Le gérant et les managers peuvent créer et révoquer des clés, pas les serveurs, caissiers ni la cuisine.

  • Une clé vaut pour un seul établissement : celui qui était choisi à sa création.
  • Lecture seule ou lecture et écriture (modifier le catalogue, faire avancer les commandes).
  • Elle ne s'affiche qu'une fois. Inklura Caisse n'en garde qu'une empreinte : une clé perdue se remplace, elle ne se retrouve pas.
  • Elle s'envoie dans l'en-tête Authorization: Bearer cbz_live_…, depuis un serveur seulement : jamais dans une page web ou une application installée.
  • Révoquée depuis la console, elle cesse de fonctionner aussitôt. 120 appels par minute et par clé ; au-delà, 429 avec Retry-After.
curl https://caisse.bzh/api/v1/sales?from=2026-09-01 \
  -H 'authorization: Bearer cbz_live_…'

API avec clé

AppelRôle
GET/api/v1/shopL'établissement de la clé, ses fonctions (commandes en ligne, stock) et les droits de la clé.
GET/api/v1/catalogueCatégories (avec leur TVA) et articles, déclinaisons et options comprises.
POST/api/v1/catalogue/itemsCréer un article : name, price TTC, categoryId, et au choix description, tags, sku, barcode.
PATCH/api/v1/catalogue/items/<id>Modifier les champs envoyés.
DELETE/api/v1/catalogue/items/<id>Supprimer un article.
GET/api/v1/stockQuantités des articles reçus d'une boutique en ligne (commerces).
GET/api/v1/salesTickets encaissés : lignes, TVA par taux, moyens de paiement. Filtres from, to, limit.
GET/api/v1/ordersCommandes en ligne, filtre status : new, accepted, ready, collected, cancelled (restaurants et bars).
POST/api/v1/orders/<id>/acceptAussi ready, collected, cancel. Accepter envoie la commande à l'écran cuisine.

Un article reçu d'une boutique en ligne porte managedBy: "sync" : il se modifie sur la boutique, la caisse refuse de le changer (409 managed_by_sync). Les ventes du mode formation portent training: true et ne comptent pas dans le chiffre d'affaires ; pour l'administration fiscale, c'est le journal scellé de la console qui fait foi.

import { createApiClient } from "./caisse-bzh.mjs";

const api = createApiClient({ apiKey: process.env.CAISSE_API_KEY });

// les ventes de septembre, pour la comptabilité
const { sales } = await api.listSales({ from: "2026-09-01", to: "2026-09-30", limit: 1000 });

// un nouvel article sur la carte
const { categories } = await api.getCatalogue();
await api.createItem({ name: "Galette complète", price: 11.5, categoryId: categories[0].id });

// les commandes à préparer
for (const o of await api.listOrders({ status: "new" })) await api.updateOrder(o.id, "accept");

Synchronisation catalogue et stock

Le protocole qu'emploie l'extension Inklura Sync pour WooCommerce, ouvert à toute boutique en ligne. La boutique possède le catalogue : elle envoie ses articles et ses mouvements de stock, la caisse renvoie ce qu'elle a vendu ou remboursé. Les articles créés à la main en caisse ne sont jamais touchés.

  1. Dans la console, ouvrez Boutique en ligne : vous y trouvez l'adresse /api/sync?shop=<jeton> et le secret partagé.
  2. Indiquez l'adresse HTTPS de votre webhook et activez l'échange.
  3. Envoyez vos articles, puis vos mouvements de stock, en appels signés.

Chaque appel est un POST JSON signé :

corps     = le JSON envoyé, tel quel
timestamp = secondes Unix (à moins de 5 minutes de l'heure de la caisse)
nonce     = 16 octets aléatoires en hexadécimal (32 caractères)
signature = HMAC-SHA256(secret, "1\n" + timestamp + "\n" + nonce + "\n" + corps), en hexadécimal

En-têtes : x-wd29-timestamp, x-wd29-nonce, x-wd29-signature

Sans signature valide, la réponse est 401 et rien n'est lu ni écrit. Le corps porte version: 1, source: "woo" (le côté boutique du protocole, quelle que soit votre plateforme) et une opération :

opEffet
healthÉtat de la liaison et file d'attente sortante. Répond même quand l'échange est coupé.
eventsJusqu'à 20 événements : product (instantané complet d'un article), stock (delta ou quantité fixée).
tickRéveille la caisse, qui renvoie aussitôt les mouvements en attente.

Les clés d'articles sont celles de votre boutique, woo:product:<id> et woo:variant:<id> avec un id entier. Chaque événement porte un id de 32 caractères hexadécimaux : un événement renvoyé deux fois n'est appliqué qu'une fois. Les quantités d'un instantané ne servent qu'au départ ; ensuite seuls les événements stock font bouger le compte.

import { createSyncClient } from "https://caisse.bzh/sdk/caisse-bzh.mjs";

const sync = createSyncClient({
  token: process.env.CAISSE_TOKEN,   // jeton de la boutique
  secret: process.env.CAISSE_SECRET, // secret partagé, jamais dans un navigateur
});

await sync.health(); // { ok: true, mode: "live", ... }

await sync.upsertProduct({
  key: "woo:product:408",
  name: "Pull marin",
  status: "publish",
  categories: [["Vêtements", "Pulls"]],
  prices: { regular: 89, tax_rate: 20 },
  identifiers: { ean13: "3760000000017" },
  variants: [
    { key: "woo:variant:409", status: "publish", attributes: { taille: "M" } },
    { key: "woo:variant:410", status: "publish", attributes: { taille: "L" } },
  ],
  inventory: [{ key: "woo:variant:409", quantity: 6 }, { key: "woo:variant:410", quantity: 4 }],
});

await sync.adjustStock("woo:variant:409", -1); // une vente sur votre site

Recevoir les ventes de la caisse

Quand la caisse vend ou rembourse un article synchronisé, elle appelle votre webhook avec la même signature, un événement à la fois et dans l'ordre (source: "ps", delta négatif pour une vente). Répondez {"ok": true} avec un statut 2xx ; sinon elle réessaie plus tard sans changer l'ordre. Vérifiez la signature avant de lire le corps, et n'appliquez pas deux fois le même id.

import { verifySignature } from "https://caisse.bzh/sdk/caisse-bzh.mjs";

// votre webhook, par exemple POST /caisse
export async function POST(request) {
  const body = await request.text(); // le corps brut, avant JSON.parse
  let msg;
  try {
    msg = await verifySignature({ secret: process.env.CAISSE_SECRET, body, headers: request.headers });
  } catch {
    return new Response("{\"ok\":false}", { status: 401 });
  }
  if (msg.op === "events") {
    for (const e of msg.events) {
      // e.key = "woo:variant:409", e.payload.delta = -1 (vente) ou +1 (remboursement)
      await appliquerUneFois(e.id, e.key, e.payload.delta);
    }
  }
  return Response.json({ ok: true });
}

SDK JavaScript

Un seul fichier, sans dépendance, fondé sur fetch et WebCrypto. Les erreurs sont levées en CaisseApiError avec status et code.

# Node 20+, Bun : copie locale, avec ses types
curl -O https://caisse.bzh/sdk/caisse-bzh.mjs
curl -O https://caisse.bzh/sdk/caisse-bzh.d.ts
import { createClient } from "./caisse-bzh.mjs";

# Deno, navigateur : import direct
import { createClient } from "https://caisse.bzh/sdk/caisse-bzh.mjs";
FonctionRôle
createApiClient()getShop(), getCatalogue(), createItem(), updateItem(), deleteItem(), getStock(), listSales(), listOrders(), updateOrder()
createClient()getCarte(slug), placeOrder(commande)
createSyncClient()health(), upsertProduct(), removeProduct(), adjustStock(), setStock(), sendEvents(), tick()
verifySignature()Contrôle un appel reçu de la caisse et renvoie le message.
signedHeaders()Les trois en-têtes de signature, pour un autre langage ou un client maison.

Dans un autre langage, générez un client depuis openapi.json ; seule la signature est à écrire à la main, en quelques lignes.

PHP et Python

La signature tient en quelques lignes dans tous les langages. Ces deux fonctions envoient un message à la caisse et vérifient un appel reçu d'elle ; elles ont été essayées telles quelles contre Inklura Caisse.

L'exemple Python fixe son User-Agent, comme le demande la règle vue dans Démarrer.

// Appel signé vers la caisse (synchronisation), PHP 7.4+ avec ext-curl.
function caisse_sync(string $token, string $secret, array $message): array {
    $body = json_encode(['version' => 1, 'source' => 'woo'] + $message, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $timestamp = (string) time();
    $nonce = bin2hex(random_bytes(16));
    $signature = hash_hmac('sha256', "1\n$timestamp\n$nonce\n$body", $secret);
    $ch = curl_init('https://caisse.bzh/api/sync?shop=' . $token);
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $body,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/json',
            "X-Wd29-Timestamp: $timestamp",
            "X-Wd29-Nonce: $nonce",
            "X-Wd29-Signature: $signature",
        ],
    ]);
    $answer = json_decode((string) curl_exec($ch), true);
    curl_close($ch);
    return is_array($answer) ? $answer : ['ok' => false];
}

// Vérifier un appel reçu de la caisse, avant de lire le corps.
function caisse_verify(string $secret, string $body, array $headers): ?array {
    $ts = $headers['x-wd29-timestamp'] ?? '';
    $nonce = $headers['x-wd29-nonce'] ?? '';
    $sig = $headers['x-wd29-signature'] ?? '';
    if (!ctype_digit($ts) || abs(time() - (int) $ts) > 300) return null;
    if (!preg_match('/^[a-f0-9]{32}$/', $nonce) || !preg_match('/^[a-f0-9]{64}$/', $sig)) return null;
    $expected = hash_hmac('sha256', "1\n$ts\n$nonce\n$body", $secret);
    if (!hash_equals($expected, $sig)) return null;
    $msg = json_decode($body, true);
    return is_array($msg) && ($msg['version'] ?? null) === 1 ? $msg : null;
}
# Appel signé vers la caisse (synchronisation), Python 3.10+ sans dépendance.
import hashlib, hmac, json, secrets, time, urllib.request

def caisse_sync(token: str, secret: str, message: dict) -> dict:
    body = json.dumps({"version": 1, "source": "woo", **message}, ensure_ascii=False, separators=(",", ":"))
    timestamp = str(int(time.time()))
    nonce = secrets.token_hex(16)
    signature = hmac.new(secret.encode(), f"1\n{timestamp}\n{nonce}\n{body}".encode(), hashlib.sha256).hexdigest()
    req = urllib.request.Request(
        "https://caisse.bzh/api/sync?shop=" + token,
        data=body.encode(),
        method="POST",
        headers={
            "Content-Type": "application/json",
            "User-Agent": "ma-boutique-sync/1.0",  # nommez votre outil (voir Démarrer)
            "X-Wd29-Timestamp": timestamp,
            "X-Wd29-Nonce": nonce,
            "X-Wd29-Signature": signature,
        },
    )
    try:
        with urllib.request.urlopen(req) as res:
            return json.load(res)
    except urllib.error.HTTPError as err:
        return json.load(err)

def caisse_verify(secret: str, body: bytes, headers) -> dict | None:
    ts, nonce, sig = headers.get("x-wd29-timestamp", ""), headers.get("x-wd29-nonce", ""), headers.get("x-wd29-signature", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300 or len(nonce) != 32 or len(sig) != 64:
        return None
    expected = hmac.new(secret.encode(), b"1\n" + ts.encode() + b"\n" + nonce.encode() + b"\n" + body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, sig):
        return None
    msg = json.loads(body)
    return msg if msg.get("version") == 1 else None

Nouveaux essais et doublons

Un appel qui crée quelque chose (POST /api/order, POST /api/v1/catalogue/items) accepte un en-tête Idempotency-Key, par exemple un UUID tiré pour chaque commande. Si la réponse se perd et que vous renvoyez la même requête avec la même clé dans les 24 heures, vous recevez la première réponse (en-tête Idempotent-Replayed: true) : pas de seconde commande en cuisine, pas d'article en double.

  • Même clé pour une requête différente : 422 idempotency_key_reused.
  • Requête identique encore en cours : 409 idempotency_in_progress, réessayez un peu plus tard.
  • Une erreur 5xx, 409 ou 429 n'est pas retenue : le nouvel essai s'exécute vraiment.
  • Les autres erreurs (400, 404, 422…) sont rejouées telles quelles pendant 24 heures : corrigez la requête et tirez une nouvelle clé.

Dans le SDK : placeOrder(commande, { idempotencyKey }) et createItem(article, { idempotencyKey }).

Erreurs et limites

StatutCodeCause
400no_slug, bad_json, no_name, no_itemsRequête incomplète ou aucune ligne reconnue.
429rate_limitedTrop de commandes pour cet établissement en 10 minutes.
404unknown_shopÉtablissement inconnu ou sans carte publique.
401unauthorizedClé API absente, inconnue ou révoquée.
403read_only_keyÉcriture avec une clé en lecture seule.
409managed_by_sync, last_itemArticle tenu par la boutique en ligne ; dernier article d'une carte.
403blockedUser-Agent par défaut d'une bibliothèque HTTP (Python, Go, Java) : nommez votre outil (voir Démarrer).
429rate_limitedPlus de 120 appels dans la minute avec la même clé.
401bridge_authJeton inconnu, signature fausse ou horodatage trop ancien.
400bridge_requestMessage invalide ; message: "disabled" si l'échange est coupé dans la console.
503bridge_requestStock modifié au même instant ailleurs : renvoyez le même événement.

Corps de 4 Mo au plus pour la synchronisation. La carte est mise en cache une minute.

Un besoin que l'API ne couvre pas encore ? Écrivez-nous : bonjour@caisse.bzh.