# DZBuild POS

DZBuild POS est la caisse Windows gratuite pour les magasins qui vendent aussi en ligne. Elle fonctionne hors ligne et, une fois reliée, reste synchronisée avec une ou plusieurs boutiques DZBuild. Cette page liste les appels d'une caisse reliée, pour que le support et les partenaires sachent ce qu'une caisse peut faire et ne peut pas faire.

Ces endpoints ne répondent qu'aux jetons délivrés à la caisse. Une clé API personnelle ou un jeton d'application reçoit `403` sur les chemins réservés au POS, et un jeton de caisse reçoit `403` sur tout chemin hors de sa liste.

## À qui elle s'adresse[​](#à-qui-elle-sadresse "Lien direct vers À qui elle s'adresse")

* Aux propriétaires qui vendent en magasin et sur leur boutique DZBuild. Seul le propriétaire de la boutique peut relier une caisse ; les membres de l'équipe ne le peuvent pas.
* À tous les plans. Une caisse n'est pas une clé API : elle ne demande pas de plan Enterprise et ne prend aucune place de clé.
* Dès que la première caisse s'enregistre, l'extension **DZBuild POS** apparaît dans le tableau de bord avec les caisses reliées, un bouton de déconnexion et les dernières ventes, retours et clôtures Z. `https://dzbuild.com/dashboard/connected-devices` ouvre cette page.

## Comment une caisse se relie[​](#comment-une-caisse-se-relie "Lien direct vers Comment une caisse se relie")

* Découverte : `GET https://dzbuild.com/.well-known/oauth-authorization-server` (RFC 8414).
* La caisse est un client OAuth 2.0 public, `dzbuild-pos-windows`, sans secret. Seul PKCE `S256` est accepté, et la redirection est `http://127.0.0.1:PORT/oauth/callback` sur n'importe quel port.
* Le propriétaire se connecte dans le navigateur du système et choisit les boutiques que la caisse peut utiliser, jusqu'à 10. Une caisse peut aussi se relier avec un code : elle en affiche un, et le propriétaire le saisit sur `https://dzbuild.com/device` depuis un téléphone.
* Le jeton d'accès commence par `dzpos_` et dure 15 minutes. Le jeton de rafraîchissement est remplacé à chaque utilisation et expire après 30 jours sans usage ou 180 jours au total. Renvoyer un ancien jeton de rafraîchissement met fin à la liaison.
* Chaque appel porte `X-DZ-Store` avec l'id de la boutique, sauf `GET /v1/me`. Une boutique que le propriétaire n'a pas choisie répond `403`.
* Chaque `POST`, `PATCH` et `DELETE` porte une `Idempotency-Key`, avec les règles de [Idempotence](https://dzbuild.com/fr/fr/api-docs/idempotency.md).
* Limites : 120 requêtes par minute par caisse et 600 par boutique. Le quota API mensuel ne s'applique pas aux caisses.
* Déconnecter la caisse dans le tableau de bord met fin à la liaison : le rafraîchissement suivant répond `400 invalid_grant` et le heartbeat suivant `410 device_revoked`.

## Les 19 scopes[​](#les-19-scopes "Lien direct vers Les 19 scopes")

La caisse demande toujours les 19, et DZBuild les accorde toujours tous.

| Scopes                                | Ce que la caisse peut faire                                                          |
| ------------------------------------- | ------------------------------------------------------------------------------------ |
| `openid`, `profile`, `offline_access` | Savoir qui l'a reliée et rester reliée                                               |
| `store:read`                          | Lire les boutiques de la liaison, leur langue, leur plan et leur plafond de produits |
| `products:read`, `products:write`     | Lire les produits, les créer et les modifier par lots, ajouter des photos            |
| `inventory:read`, `inventory:write`   | Lire et ajuster le stock des produits                                                |
| `orders:read`, `orders:write`         | Recevoir les commandes de la boutique, en prendre une, la faire avancer, l'annuler   |
| `customers:read`                      | Vérifier un numéro de téléphone avant une vente                                      |
| `pos:sales:read`, `pos:sales:write`   | Enregistrer ventes, retours et clôtures Z                                            |
| `locations:read`, `locations:write`   | Enregistrer le magasin où se trouve la caisse                                        |
| `backups:read`, `backups:write`       | Réservés : les sauvegardes ne sont pas proposées                                     |
| `devices:self`                        | Enregistrer la caisse, envoyer des heartbeats, se délier                             |
| `events:read`                         | Lire le flux des changements                                                         |

## Endpoints[​](#endpoints "Lien direct vers Endpoints")

| Méthode et chemin                                                                     | Rôle                                                                                                       |
| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `GET /v1/me`                                                                          | Le propriétaire et les boutiques de la liaison                                                             |
| `GET /v1/store`                                                                       | La boutique choisie : nom, langue, devise `DZD`, moment de déduction du stock, plan et plafond de produits |
| `POST /v1/devices`, `GET /v1/devices`                                                 | Enregistrer la caisse (une ligne par caisse et par boutique), lister les caisses                           |
| `POST /v1/devices/{id}/heartbeat`                                                     | Toutes les 15 minutes : version, envois en attente et en échec                                             |
| `DELETE /v1/devices/{id}`                                                             | Délier cette caisse                                                                                        |
| `GET /v1/locations`, `POST /v1/locations`                                             | Le magasin où se trouve la caisse                                                                          |
| `POST /v1/products/batch`                                                             | Créer ou modifier jusqu'à 100 produits                                                                     |
| `GET /v1/products`, `GET /v1/products/{id}`                                           | Produits modifiés depuis une date, un produit                                                              |
| `POST /v1/media/uploads`, `POST /v1/products/{id}/images`                             | Ticket photo, puis rattacher la photo envoyée                                                              |
| `POST /v1/inventory/adjustments/batch`                                                | Jusqu'à 500 fixations ou variations de stock                                                               |
| `POST /v1/pos/sales`, `POST /v1/pos/sales/{sale_id}/refunds`, `POST /v1/pos/closures` | Ventes, retours, clôtures Z                                                                                |
| `GET /v1/orders`, `GET /v1/orders/{id}`                                               | Les commandes de la boutique au format de la caisse                                                        |
| `POST /v1/orders/{id}/claim`, `PATCH /v1/orders/{id}`, `POST /v1/orders/{id}/cancel`  | Prendre une commande, la faire avancer, l'annuler                                                          |
| `GET /v1/customers?phone=`                                                            | Les indicateurs d'un numéro de téléphone                                                                   |
| `GET /v1/events`                                                                      | Le flux des changements                                                                                    |

Certains chemins sont partagés avec les clés API (produits, commandes, clients). Une caisse reçoit les formats de cette page ; une clé API garde les formats de la section Ressources.

## Lot de produits[​](#lot-de-produits "Lien direct vers Lot de produits")

`POST /v1/products/batch` prend `items`, jusqu'à 100. Chaque élément porte le `external_id` propre à la caisse, `name`, un `sku` et un `barcode` facultatifs, `pricing.price` en chaîne à deux décimales (`"4500.00"`), `inventory.track_stock`, `status` (`active`, `draft` ou `archived`) et une `category` facultative avec son propre `external_id` et son `name`.

* Un élément modifie le produit déjà relié à son `external_id`. Sinon, il reprend un produit sans variantes qui a le même `sku` et aucune liaison. Sinon, il crée un produit avec 0 en stock.
* Une catégorie est retrouvée par son `external_id`, ou créée à partir de son nom.
* La réponse liste chaque élément : `external_id`, `id`, `status` (`created` ou `updated`) et `error: null`. Un élément refusé porte un objet `error` et ni `id` ni `status`.
* Plafond du plan : quand la boutique a déjà autant de produits actifs que son plan le permet, un lot qui créerait un produit, brouillon ou non, répond `402 product_limit_reached`. Les éléments placés avant restent écrits. Passer un produit existant en `active` au-delà du plafond est une erreur sur cet élément seulement.
* Le lot ne fixe jamais le stock. Le stock passe par l'appel d'ajustements.

## Photos[​](#photos "Lien direct vers Photos")

1. `POST /v1/media/uploads` avec `filename`, `content_type` (`image/jpeg`, `image/png` ou `image/webp`), `size` (jusqu'à 8 Mio) et `sha256`. La réponse porte `media_id` et une `upload_url` signée valable 10 minutes.
2. La caisse envoie les octets bruts en `PUT` sur `upload_url`, sans les en-têtes DZBuild.
3. `POST /v1/products/{id}/images` avec `media_id` et `position` rattache la photo. La taille et le sha256 doivent correspondre au ticket, sinon l'appel répond `422 media_mismatch`. Un ticket inconnu ou expiré répond `404 media_not_found`. Un produit garde jusqu'à 20 photos.

## Stock[​](#stock "Lien direct vers Stock")

`POST /v1/inventory/adjustments/batch` prend jusqu'à 500 éléments. Chaque élément indique `product_external_id`, `target: "product"`, soit `set` (pièces entières) soit `delta`, une `reason` (`pos_sale`, `pos_return`, `restock`, `count` ou `loss`) et une `ref` unique.

* Seuls les produits qui suivent le stock au niveau du produit peuvent être ajustés. Un produit avec un stock par variante répond l'erreur d'élément `variant_product`, un produit sans suivi de stock `not_tracked`, un produit qui n'existe plus `unknown_product`.
* La réponse liste chaque élément par `ref` avec `status` `ok` ou `error`.
* Les changements de stock de la caisse n'apparaissent jamais dans l'historique des modifications du tableau de bord et ne proposent jamais d'annulation.

## Documents POS[​](#documents-pos "Lien direct vers Documents POS")

`POST /v1/pos/sales` enregistre un ticket, une facture ou un bon de livraison ; `POST /v1/pos/sales/{sale_id}/refunds` un bon de retour ou un avoir sur cette vente ; `POST /v1/pos/closures` une clôture Z avec ses totaux et ses ancres d'empreinte.

* Les documents sont gardés tels qu'envoyés et ne changent jamais : il n'existe aucun chemin de modification ou de suppression.
* Une vente répond `201` avec `{"id": 99120, "stock_applied": false}`. Le stock passe par l'appel d'ajustements, jamais par un document.
* Les documents POS sont séparés des commandes. Ils ne comptent pas dans la limite mensuelle de commandes de la boutique, ne notifient personne et n'atteignent jamais Google Sheets ni les webhooks.
* Les montants sont des chaînes à deux décimales, les quantités des nombres à 3 décimales au plus, avec jusqu'à 500 lignes et 20 paiements par document.
* Envoyer un document dont le `external_id` est déjà enregistré répond `409 already_exists` avec l'id enregistré dans `error.details.id`. La caisse le lit comme un succès.
* Un retour sur une vente d'une autre boutique répond `404 not_found`.

## Commandes[​](#commandes "Lien direct vers Commandes")

* `GET /v1/orders?updated_since=...` et `GET /v1/orders/{id}` donnent les commandes de la boutique au format de la caisse, avec leurs articles. `order_number` est le numéro court de la boutique quand il existe.
* `POST /v1/orders/{id}/claim` avec `device_id` et `terminal` : la première caisse l'emporte. La même caisse qui la reprend reçoit `200` ; une autre caisse reçoit `409 order_claimed`.
* `PATCH /v1/orders/{id}` avec `status` demande la prise (`409 claim_required` sinon). Passages permis : de `pending` ou `confirmed` vers `processing`, `shipped` ou `delivered`, de `processing` vers `shipped` ou `delivered`, et de `shipped` vers `delivered`. Tout autre passage répond `409 transition_not_allowed`.
* `POST /v1/orders/{id}/cancel` prend `reason` (`out_of_stock`, `customer_unreachable`, `duplicate` ou `other`) et une `note` facultative de 500 caractères au plus. Le stock revient selon les règles de stock de la boutique. Si une autre caisse tient la prise, l'annulation répond `409 order_claimed`.
* Un changement de statut depuis la caisse déclenche les mêmes suites qu'un changement fait dans le tableau de bord, notifications et mise à jour Google Sheets comprises.

## Clients[​](#clients "Lien direct vers Clients")

`GET /v1/customers?phone=0550123456` répond une page d'un élément au plus : `id`, `is_banned` et `fraud_score`. Elle ne regarde que les clients de la boutique, accepte le numéro avec ou sans `+213`, et ne donne ni nom, ni adresse, ni historique. Un numéro inconnu donne une page vide.

## Événements[​](#événements "Lien direct vers Événements")

`GET /v1/events?wait=25&limit=200&cursor=...` renvoie `items`, `next_cursor` et `has_more`. `next_cursor` est toujours présent, même sur une page vide ; la caisse le renvoie à l'appel suivant.

* La périphérie garde l'appel jusqu'à 25 secondes et répond dès qu'un changement arrive.
* Le premier appel, sans curseur, commence par un `order.updated` pour chaque commande `pending`, `confirmed` et `processing`.
* Types : `order.created`, `order.updated`, `product.updated`, `product.deleted`, `inventory.level_changed`, `customer.updated` et `device.revoked`. Chaque événement a un `id` stable, ce qui permet à la caisse d'ignorer les doublons.
* `inventory.level_changed` porte `old`, `new`, `delta` et une `source` : `order` quand une commande a fait bouger le stock (avec le numéro de commande), `dashboard` pour tout autre changement fait hors de la caisse, et `pos` quand une autre caisse de la même boutique l'a changé (la caisse ignore cette source).
* Les écritures de produits et de stock de la caisse elle-même ne lui sont pas renvoyées.
* `device.revoked` porte `device_id` en chaîne, une seule fois, après la déconnexion de la caisse.
* Un curseur que cette API n'a pas émis répond `400 bad_request`.

## Sauvegardes[​](#sauvegardes "Lien direct vers Sauvegardes")

DZBuild ne garde pas les sauvegardes des caisses. `GET /v1/backups`, `POST /v1/backups`, `POST /v1/backups/{id}/complete` et `GET /v1/backups/{id}/download` répondent toujours `501 not_implemented`, et la caisse garde ses sauvegardes sur le PC.

## Codes d'erreur[​](#codes-derreur "Lien direct vers Codes d'erreur")

| Statut | `code`                   | Quand                                                                                        |
| ------ | ------------------------ | -------------------------------------------------------------------------------------------- |
| 400    | `bad_request`            | Un `updated_since` incorrect ou un curseur que cette API n'a pas émis                        |
| 401    | `unauthorized`           | Jeton absent, faux ou expiré, ou liaison terminée                                            |
| 402    | `product_limit_reached`  | Un lot créerait un produit au-delà du plafond du plan                                        |
| 403    | `forbidden`              | Un chemin hors de la liste de la caisse, ou une boutique que le propriétaire n'a pas choisie |
| 404    | `not_found`              | Produit, vente ou commande absent de cette boutique                                          |
| 404    | `device_not_found`       | Un id de caisse qui n'est pas cette caisse sur cette boutique                                |
| 404    | `media_not_found`        | Ticket photo inconnu ou expiré                                                               |
| 409    | `already_exists`         | Un document avec ce `external_id` est enregistré ; son id est dans `details.id`              |
| 409    | `order_claimed`          | Une autre caisse a pris la commande                                                          |
| 409    | `claim_required`         | Faire avancer une commande que cette caisse n'a pas prise                                    |
| 409    | `transition_not_allowed` | Le passage n'est pas permis depuis le statut de la commande                                  |
| 410    | `device_revoked`         | La caisse a été déconnectée                                                                  |
| 413    | `payload_too_large`      | Corps de plus de 1 Mio                                                                       |
| 422    | `validation_error`       | Un champ est incorrect ; `details.field` le nomme                                            |
| 422    | `media_mismatch`         | La photo envoyée ne correspond pas à son ticket                                              |
| 422    | `idempotency_key_reuse`  | Même `Idempotency-Key` avec un autre corps                                                   |
| 429    | `rate_limited`           | Limite dépassée ; attendre `Retry-After`                                                     |
| 501    | `not_implemented`        | Les chemins de sauvegarde                                                                    |
| 503    | `storage_unavailable`    | Le stockage des photos est injoignable ; réessayer plus tard                                 |
