API مفتوحة

اربط برنامجك بأرض المتجر.

واجهة REST بسيطة لمزامنة المنتجات والأسعار، واستقبال الجرد، ودفع تحديثات فورية إلى أجهزة العرض.

REST · JSON · UTF-83 أنماط مصادقةPHP · Node · C# · Java · Python · Delphi · WinDevبدون رسوم
Votre logiciel
POS / Gestion
⇠ lit /api/productspush_prices ⇢
izySCAN
Server
⇢ Wi-Fi LAN ⇠
A1 · TWIN
X7 · PDA

Vue d'Ensemble

Ce document fournit les spécifications techniques pour intégrer les systèmes de vente avec IzyScan Server. IzyScan est un système d'affichage des prix et de gestion des stocks qui nécessite une synchronisation avec les systèmes POS/vente existants.

💡 Note: IzyScan supporte plusieurs méthodes d'authentification et offre une API RESTful complète pour une intégration transparente.

🖥️ Démarrage du Serveur Web API

Cette section vous montre comment créer et démarrer un serveur web simple sur le port 80 dans différents langages de programmation.

Création d'un Serveur Web Basique

PHP - Serveur Intégré

// server.php
<?php
// Route principale
if ($_SERVER['REQUEST_URI'] == '/') {
    header('Content-Type: application/json');
    echo json_encode(['status' => 'success', 'message' => 'Serveur en marche']);
    exit;
}

// Autres routes retournent 404
http_response_code(404);
echo json_encode(['status' => 'error', 'message' => 'Route non trouvée']);

// Pour démarrer le serveur sur le port 80:
// sudo php -S 0.0.0.0:80 server.php
?>

Node.js - Serveur HTTP

// server.js
const http = require('http');

const server = http.createServer((req, res) => {
    res.setHeader('Content-Type', 'application/json');
    
    if (req.url === '/' && req.method === 'GET') {
        res.statusCode = 200;
        res.end(JSON.stringify({
            status: 'success',
            message: 'Serveur en marche'
        }));
    } else {
        res.statusCode = 404;
        res.end(JSON.stringify({
            status: 'error',
            message: 'Route non trouvée'
        }));
    }
});

const PORT = 80;
server.listen(PORT, () => {
    console.log(`Serveur démarré sur http://localhost:${PORT}`);
});

// Pour démarrer: sudo node server.js

C# - Serveur HTTP Simple

// Program.cs
using System;
using System.Net;
using System.Text;
using System.Text.Json;

class Program
{
    static void Main()
    {
        HttpListener listener = new HttpListener();
        listener.Prefixes.Add("http://*:80/");
        listener.Start();
        
        Console.WriteLine("Serveur démarré sur http://localhost:80");
        
        while (true)
        {
            HttpListenerContext context = listener.GetContext();
            HttpListenerResponse response = context.Response;
            
            response.ContentType = "application/json";
            
            string responseString;
            if (context.Request.Url.AbsolutePath == "/")
            {
                responseString = JsonSerializer.Serialize(new {
                    status = "success",
                    message = "Serveur en marche"
                });
                response.StatusCode = 200;
            }
            else
            {
                responseString = JsonSerializer.Serialize(new {
                    status = "error",
                    message = "Route non trouvée"
                });
                response.StatusCode = 404;
            }
            
            byte[] buffer = Encoding.UTF8.GetBytes(responseString);
            response.ContentLength64 = buffer.Length;
            response.OutputStream.Write(buffer, 0, buffer.Length);
            response.Close();
        }
    }
}

// Pour compiler et démarrer:
// dotnet run (en tant qu'administrateur)

Java - Serveur HTTP

// SimpleServer.java
import com.sun.net.httpserver.*;
import java.io.*;
import java.net.InetSocketAddress;

public class SimpleServer {
    public static void main(String[] args) throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress(80), 0);
        
        server.createContext("/", exchange -> {
            String response;
            int statusCode;
            
            if (exchange.getRequestURI().getPath().equals("/")) {
                response = "{\"status\":\"success\",\"message\":\"Serveur en marche\"}";
                statusCode = 200;
            } else {
                response = "{\"status\":\"error\",\"message\":\"Route non trouvée\"}";
                statusCode = 404;
            }
            
            exchange.getResponseHeaders().set("Content-Type", "application/json");
            exchange.sendResponseHeaders(statusCode, response.length());
            
            OutputStream os = exchange.getResponseBody();
            os.write(response.getBytes());
            os.close();
        });
        
        server.start();
        System.out.println("Serveur démarré sur http://localhost:80");
    }
}

// Pour compiler et démarrer:
// javac SimpleServer.java
// sudo java SimpleServer

Python - Serveur HTTP

# server.py
from http.server import HTTPServer, BaseHTTPRequestHandler
import json

class SimpleHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200 if self.path == '/' else 404)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        
        if self.path == '/':
            response = {
                'status': 'success',
                'message': 'Serveur en marche'
            }
        else:
            response = {
                'status': 'error',
                'message': 'Route non trouvée'
            }
        
        self.wfile.write(json.dumps(response).encode())

if __name__ == '__main__':
    server = HTTPServer(('', 80), SimpleHandler)
    print('Serveur démarré sur http://localhost:80')
    server.serve_forever()

# Pour démarrer: sudo python server.py

Delphi - Serveur HTTP avec Indy

program SimpleWebServer;

{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  IdHTTPServer,
  IdContext,
  IdCustomHTTPServer;

type
  TSimpleServer = class
    procedure HandleRequest(AContext: TIdContext; 
      ARequestInfo: TIdHTTPRequestInfo; 
      AResponseInfo: TIdHTTPResponseInfo);
  end;

procedure TSimpleServer.HandleRequest(AContext: TIdContext;
  ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
  AResponseInfo.ContentType := 'application/json';
  
  if ARequestInfo.Document = '/' then
  begin
    AResponseInfo.ResponseNo := 200;
    AResponseInfo.ContentText := '{"status":"success","message":"Serveur en marche"}';
  end
  else
  begin
    AResponseInfo.ResponseNo := 404;
    AResponseInfo.ContentText := '{"status":"error","message":"Route non trouvée"}';
  end;
end;

var
  Server: TIdHTTPServer;
  Handler: TSimpleServer;
begin
  Server := TIdHTTPServer.Create(nil);
  Handler := TSimpleServer.Create;
  try
    Server.DefaultPort := 80;
    Server.OnCommandGet := Handler.HandleRequest;
    Server.Active := True;
    
    WriteLn('Serveur démarré sur http://localhost:80');
    WriteLn('Appuyez sur Entrée pour arrêter...');
    ReadLn;
  finally
    Server.Free;
    Handler.Free;
  end;
end.

WinDev - Serveur Web Simple

// Procédure principale
PROCEDURE DémarrerServeur()
    // Créer un serveur web sur le port 80
    MonServeur est un httpServeur
    MonServeur..Port = 80
    MonServeur..RacineDocumentaire = fRepExe()
    
    // Ajouter une route pour "/"
    MonServeur..AjouteRoute("/", ProcédureAccueil)
    
    // Démarrer le serveur
    SI MonServeur..Démarre() ALORS
        Info("Serveur démarré sur http://localhost:80")
    SINON
        Erreur("Impossible de démarrer le serveur")
    FIN
FIN

// Procédure pour gérer la route principale
PROCEDURE ProcédureAccueil(Requête est un httpRequête, Réponse est un httpRéponse)
    Réponse..TypeContenu = "application/json"
    Réponse..CodeStatut = 200
    
    vRéponse est un Variant
    vRéponse.status = "success"
    vRéponse.message = "Serveur en marche"
    
    Réponse..Contenu = VariantVersJSON(vRéponse)
FIN

// Appel de la procédure
DémarrerServeur()
💡 Notes Importantes:
  • Le port 80 nécessite généralement des privilèges administrateur/root
  • Sur Linux/Mac, utilisez sudo pour démarrer le serveur
  • Sur Windows, exécutez en tant qu'administrateur
  • Pour tester: ouvrez votre navigateur sur http://localhost/

🔐 Authentification

IzyScan supporte trois méthodes d'authentification (configurables par installation):

1. Sans Authentification

Si les champs login et mot de passe sont vides, les requêtes sont envoyées sans en-têtes d'authentification.

2. Authentification par Token

Si seul le champ mot de passe est configuré, il est traité comme un token d'accès:

Authorization: Bearer votre_token_acces_ici

3. Authentification Basique

Si le login et le mot de passe sont fournis:

Authorization: Basic base64(nom_utilisateur:mot_de_passe)

🏓 Point de Terminaison Ping (Health Check)

Objectif: Tester la connectivité avec le serveur

GET /api/ping

Vérification de Santé

Ce point de terminaison permet à IzyScan de vérifier que le serveur est accessible et répond correctement.

Réponse:
Statut Description
200 OK Le corps de la réponse peut être n'importe quoi (non analysé par le client)
💡 Note: Le contenu de la réponse n'est pas analysé par le client. Seul le code de statut HTTP 200 est vérifié.

📝 Exemples de Code

// PHP - GET /api/ping
public function ping() {
    return response()->json([
        'status' => 'ok',
        'message' => 'pong'
    ], 200);
}

// Dans routes/api.php
Route::get('/api/ping', 'ApiController@ping');
// Node.js - GET /api/ping
app.get('/api/ping', (req, res) => {
  res.status(200).json({
    status: 'ok',
    message: 'pong'
  });
});
// C# - GET /api/ping
[HttpGet("ping")]
public IActionResult Ping()
{
    return Ok(new
    {
        status = "ok",
        message = "pong"
    });
}
// Java - GET /api/ping
@GetMapping("/api/ping")
public ResponseEntity ping() {
    return ResponseEntity.ok(Map.of(
        "status", "ok",
        "message", "pong"
    ));
}
# Python - GET /api/ping
@app.route('/api/ping', methods=['GET'])
def ping():
    return jsonify({
        'status': 'ok',
        'message': 'pong'
    }), 200
// Delphi - GET /api/ping
procedure TSimpleServer.HandlePing(AContext: TIdContext;
  ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
  AResponseInfo.ContentType := 'application/json';
  AResponseInfo.ResponseNo := 200;
  AResponseInfo.ContentText := '{"status":"ok","message":"pong"}';
end;
// WinDev - GET /api/ping
PROCEDURE ProcédurePing(Requête est un httpRequête, Réponse est un httpRéponse)
    Réponse..TypeContenu = "application/json"
    Réponse..CodeStatut = 200

    vRéponse est un Variant
    vRéponse.status = "ok"
    vRéponse.message = "pong"

    Réponse..Contenu = VariantVersJSON(vRéponse)
FIN

📦 Points de Terminaison Produits

Tous les points de terminaison doivent accepter du contenu JSON et retourner des réponses JSON.

⚡ En-têtes Requis: Content-Type: application/json et Accept: application/json
GET /api/products

Synchronisation des Produits

Objectif: Récupérer les produits pour la synchronisation avec IzyScan

Paramètres de Requête:
Paramètre Type Obligatoire Description
updated_since string (ISO 8601) Optionnel Pour la synchronisation incrémentale (ex: 2024-01-15 10:30:00). Retourne uniquement les produits créés ou modifiés après cette date.
limit integer Optionnel Nombre maximum de produits par requête (le client demande 2000)
offset integer Optionnel Nombre de produits à ignorer (pour la pagination, défaut: 0)
Exemple de Requête:
GET /api/products?updated_since=2024-01-15 10:30:00&limit=2000&offset=0
Réponse:
{
  "data": [
    {
      "id": 123,
      "name": "Nom du Produit",
      "barcode": "1234567890123",
      "price": 29.99,
      "promo_price": 25.99,
      "server_id": 123
    }
  ],
  "pagination": {
    "total": 15000,
    "offset": 0,
    "limit": 2000,
    "has_more": true
  }
}
Champs de l'objet Produit:
Champ Type Obligatoire Description
id string/number Obligatoire Identifiant unique du produit (peut être identique au barcode)
name string Obligatoire Nom d'affichage du produit
barcode string Obligatoire Code-barres du produit (EAN-13, UPC, etc.). Les produits sans code-barres sont ignorés.
price number Obligatoire Prix de vente actuel
promo_price number Optionnel Prix promotionnel (si applicable)
server_id string/number Optionnel ID du produit côté serveur (utilisé pour les mises à jour de prix)
Champs de l'objet Pagination:
Champ Type Obligatoire Description
total integer Recommandé Nombre total de produits correspondant à la requête
offset integer Recommandé Décalage actuel
limit integer Recommandé Limite demandée
has_more boolean Recommandé true si plus de produits disponibles, false sinon
💡 Notes:
  • Si la pagination n'est pas fournie, le client utilise la détection de taille de lot
  • Les produits avec un code-barres vide ou manquant sont ignorés par le client
  • Le client récupère par lots de 2000 jusqu'à ce que has_more soit false

📝 Exemples de Code

// PHP - GET /api/products
public function getProducts(Request $request) {
    $updatedSince = $request->query('updated_since');
    $limit = min($request->query('limit', 2000), 2000);
    $offset = $request->query('offset', 0);

    $query = Product::where('barcode', '!=', null)
                    ->where('barcode', '!=', '');

    if ($updatedSince) {
        $query->where('updated_at', '>=', $updatedSince);
    }

    $total = $query->count();
    $products = $query->offset($offset)->limit($limit)->get();

    return response()->json([
        'data' => $products->map(function($product) {
            return [
                'id' => $product->id,
                'name' => $product->name,
                'barcode' => $product->barcode,
                'price' => (float) $product->price,
                'promo_price' => $product->promo_price ? (float) $product->promo_price : null,
                'server_id' => $product->id,
            ];
        }),
        'pagination' => [
            'total' => $total,
            'offset' => $offset,
            'limit' => $limit,
            'has_more' => ($offset + $limit) < $total,
        ]
    ]);
}
// Node.js - GET /api/products
app.get('/api/products', async (req, res) => {
  const { updated_since, limit = 2000, offset = 0 } = req.query;

  let query = db.products.find({
    barcode: { $ne: null, $ne: '' }
  });

  if (updated_since) {
    query = query.where('updated_at').gte(new Date(updated_since));
  }

  const total = await db.products.countDocuments({
    barcode: { $ne: null, $ne: '' }
  });

  const products = await query
    .skip(parseInt(offset))
    .limit(Math.min(parseInt(limit), 2000))
    .exec();

  res.json({
    data: products.map(product => ({
      id: product._id,
      name: product.name,
      barcode: product.barcode,
      price: parseFloat(product.price),
      promo_price: product.promo_price ? parseFloat(product.promo_price) : null,
      server_id: product._id,
    })),
    pagination: {
      total,
      offset: parseInt(offset),
      limit: parseInt(limit),
      has_more: (parseInt(offset) + parseInt(limit)) < total,
    }
  });
});
// C# - GET /api/products
[HttpGet("products")]
public async Task GetProducts(
    [FromQuery] DateTime? updatedSince,
    [FromQuery] int limit = 2000,
    [FromQuery] int offset = 0)
{
    limit = Math.Min(limit, 2000);

    var query = _context.Products
        .Where(p => p.Barcode != null && p.Barcode != "");

    if (updatedSince.HasValue)
    {
        query = query.Where(p => p.UpdatedAt >= updatedSince.Value);
    }

    var total = await query.CountAsync();

    var products = await query
        .Skip(offset)
        .Take(limit)
        .Select(p => new
        {
            id = p.Id,
            name = p.Name,
            barcode = p.Barcode,
            price = p.Price,
            promo_price = (decimal?)p.PromoPrice,
            server_id = p.Id
        })
        .ToListAsync();

    return Ok(new
    {
        data = products,
        pagination = new
        {
            total,
            offset,
            limit,
            has_more = (offset + limit) < total
        }
    });
}
// Java - GET /api/products
@GetMapping("/api/products")
public ResponseEntity getProducts(
    @RequestParam(required = false) String updatedSince,
    @RequestParam(defaultValue = "2000") int limit,
    @RequestParam(defaultValue = "0") int offset) {

    limit = Math.min(limit, 2000);

    Specification spec = (root, query, cb) ->
        cb.and(
            cb.isNotNull(root.get("barcode")),
            cb.notEqual(root.get("barcode"), "")
        );

    if (updatedSince != null) {
        LocalDateTime date = LocalDateTime.parse(updatedSince);
        spec = spec.and((root, query, cb) ->
            cb.greaterThanOrEqualTo(root.get("updatedAt"), date));
    }

    long total = productRepository.count(spec);
    Pageable pageable = PageRequest.of(offset / limit, limit);
    Page page = productRepository.findAll(spec, pageable);

    List> products = page.getContent().stream()
        .map(p -> Map.of(
            "id", p.getId(),
            "name", p.getName(),
            "barcode", p.getBarcode(),
            "price", p.getPrice(),
            "promo_price", p.getPromoPrice(),
            "server_id", p.getId()
        ))
        .collect(Collectors.toList());

    return ResponseEntity.ok(Map.of(
        "data", products,
        "pagination", Map.of(
            "total", total,
            "offset", offset,
            "limit", limit,
            "has_more", offset + limit < total
        )
    ));
}
# Python - GET /api/products
from flask import Flask, request, jsonify
from datetime import datetime

@app.route('/api/products', methods=['GET'])
def get_products():
    updated_since = request.args.get('updated_since')
    limit = min(int(request.args.get('limit', 2000)), 2000)
    offset = int(request.args.get('offset', 0))

    query = Product.query.filter(
        Product.barcode.isnot(None),
        Product.barcode != ''
    )

    if updated_since:
        date = datetime.fromisoformat(updated_since.replace('Z', '+00:00'))
        query = query.filter(Product.updated_at >= date)

    total = query.count()
    products = query.offset(offset).limit(limit).all()

    return jsonify({
        'data': [{
            'id': p.id,
            'name': p.name,
            'barcode': p.barcode,
            'price': float(p.price),
            'promo_price': float(p.promo_price) if p.promo_price else None,
            'server_id': p.id
        } for p in products],
        'pagination': {
            'total': total,
            'offset': offset,
            'limit': limit,
            'has_more': (offset + limit) < total
        }
    })
// Delphi - GET /api/products
procedure GetProducts(const UpdatedSince: string; Limit, Offset: Integer);
var
  HttpClient: TNetHTTPClient;
  Response: IHTTPResponse;
  URL: string;
  JSONResponse: TJSONObject;
  Products: TJSONArray;
begin
  HttpClient := TNetHTTPClient.Create(nil);
  try
    URL := Format('https://api.example.com/api/products?limit=%d&offset=%d',
                  [Limit, Offset]);
    
    if UpdatedSince <> '' then
      URL := URL + '&updated_since=' + UpdatedSince;
    
    HttpClient.Accept := 'application/json';
    Response := HttpClient.Get(URL);
    
    if Response.StatusCode = 200 then
    begin
      JSONResponse := TJSONObject.ParseJSONValue(Response.ContentAsString) as TJSONObject;
      try
        if JSONResponse.GetValue('status').Value = 'success' then
        begin
          Products := JSONResponse.GetValue('data');
          // Traiter les produits...
        end;
      finally
        JSONResponse.Free;
      end;
    end;
  finally
    HttpClient.Free;
  end;
end;
// WinDev - GET /api/products
PROCEDURE RécupérerProduits(dtDepuisMiseAJour est un DateHeure = "", nLimite est un entier = 2000, nOffset est un entier = 0)
    tabProduits est un tableau de STProduct

    sURL = gsURLAPI + "/api/products?"
    sURL += "limit=" + nLimite
    sURL += "&offset=" + nOffset

    SI dtDepuisMiseAJour <> "" ALORS
        sURL += "&updated_since=" + DateHeureVersISO8601(dtDepuisMiseAJour)
    FIN

    requeteHTTP est un httpRequête
    requeteHTTP.URL = sURL
    requeteHTTP.Méthode = httpGet

    tabEntetes est un tableau associatif de chaînes = ObtenirEntetes()
    POUR TOUT sValeur, sCle DE tabEntetes
        requeteHTTP.Entête[sCle] = sValeur
    FIN

    réponseHTTP est un httpRéponse = HTTPEnvoie(requeteHTTP)

    SI réponseHTTP.CodeEtat = 200 ALORS
        vJSON est un Variant = JSONVersVariant(réponseHTTP.Contenu)

        POUR TOUT vProduit DE vJSON.data
            stProduit est un STProduct
            stProduit.id = vProduit.id
            stProduit.name = vProduit.name
            stProduit.barcode = vProduit.barcode
            stProduit.price = vProduit.price
            stProduit.promo_price = vProduit.promo_price
            stProduit.server_id = vProduit.server_id

            Ajoute(tabProduits, stProduit)
        FIN
    FIN

    RENVOYER tabProduits
FIN
PUT POST /api/products/{barcode}/price

Mise à Jour du Prix

Objectif: Mettre à jour le prix d'un produit depuis IzyScan

💡 Note: Ce point de terminaison accepte les méthodes PUT et POST pour une compatibilité maximale.
Paramètres d'URL:
  • barcode: Code-barres du produit
Corps de la Requête:
{
  "barcode": "1234567890123",
  "price": 32.99,
  "server_id": 123
}
Réponse:
{
  "status": "success",
  "message": "Prix mis à jour avec succès",
  "data": {
    "server_id": 123,
    "barcode": "1234567890123",
    "old_price": 29.99,
    "new_price": 32.99,
    "updated_at": "2025-07-16T15:30:00Z"
  }
}

📝 Exemples de Code

// PHP - PUT ou POST /api/products/{barcode}/price
public function updatePrice(Request $request, $barcode) {
    $validated = $request->validate([
        'price' => 'required|numeric|min:0',
        'server_id' => 'required|integer'
    ]);
    
    $product = Product::where('barcode', $barcode)->first();
    
    if (!$product) {
        return response()->json([
            'status' => 'error',
            'error_code' => 'PRODUCT_NOT_FOUND',
            'message' => 'Produit non trouvé'
        ], 404);
    }
    
    $oldPrice = $product->price;
    $product->price = $validated['price'];
    $product->save();
    
    return response()->json([
        'status' => 'success',
        'message' => 'Prix mis à jour avec succès',
        'data' => [
            'server_id' => $product->id,
            'barcode' => $product->barcode,
            'old_price' => (float) $oldPrice,
            'new_price' => (float) $product->price,
            'updated_at' => $product->updated_at->toISOString(),
        ]
    ]);
}

// Dans routes/api.php
Route::match(['PUT', 'POST'], '/api/products/{barcode}/price', 'ProductController@updatePrice');
// Node.js - PUT ou POST /api/products/:barcode/price
app.put('/api/products/:barcode/price', updatePrice);
app.post('/api/products/:barcode/price', updatePrice);

async function updatePrice(req, res) {
  const { barcode } = req.params;
  const { price, server_id } = req.body;
  
  if (!price || price < 0) {
    return res.status(400).json({
      status: 'error',
      error_code: 'INVALID_PRICE',
      message: 'Le prix doit être un nombre positif'
    });
  }
  
  const product = await db.products.findOne({ barcode });
  
  if (!product) {
    return res.status(404).json({
      status: 'error',
      error_code: 'PRODUCT_NOT_FOUND',
      message: 'Produit non trouvé'
    });
  }
  
  const oldPrice = product.price;
  product.price = price;
  product.updated_at = new Date();
  await product.save();
  
  res.json({
    status: 'success',
    message: 'Prix mis à jour avec succès',
    data: {
      server_id: product._id,
      barcode: product.barcode,
      old_price: parseFloat(oldPrice),
      new_price: parseFloat(price),
      updated_at: product.updated_at.toISOString(),
    }
  });
}
// C# - PUT ou POST /api/products/{barcode}/price
[HttpPut("products/{barcode}/price")]
[HttpPost("products/{barcode}/price")]
public async Task UpdatePrice(
    string barcode,
    [FromBody] PriceUpdateRequest request)
{
    if (request.Price < 0)
    {
        return BadRequest(new ErrorResponse
        {
            ErrorCode = "INVALID_PRICE",
            Message = "Le prix doit être un nombre positif"
        });
    }
    
    var product = await _context.Products
        .FirstOrDefaultAsync(p => p.Barcode == barcode);
    
    if (product == null)
    {
        return NotFound(new ErrorResponse
        {
            ErrorCode = "PRODUCT_NOT_FOUND",
            Message = "Produit non trouvé"
        });
    }
    
    var oldPrice = product.Price;
    product.Price = request.Price;
    product.UpdatedAt = DateTime.UtcNow;
    
    await _context.SaveChangesAsync();
    
    return Ok(new
    {
        status = "success",
        message = "Prix mis à jour avec succès",
        data = new
        {
            server_id = product.Id,
            barcode = product.Barcode,
            old_price = oldPrice,
            new_price = product.Price,
            updated_at = product.UpdatedAt.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")
        }
    });
}
// Java - PUT ou POST /api/products/{barcode}/price
@PutMapping("/api/products/{barcode}/price")
@PostMapping("/api/products/{barcode}/price")
public ResponseEntity updatePrice(
    @PathVariable String barcode,
    @RequestBody PriceUpdateRequest request) {
    
    if (request.getPrice().compareTo(BigDecimal.ZERO) < 0) {
        return ResponseEntity.badRequest().body(Map.of(
            "status", "error",
            "error_code", "INVALID_PRICE",
            "message", "Le prix doit être un nombre positif"
        ));
    }
    
    Optional productOpt = productRepository.findByBarcode(barcode);
    
    if (productOpt.isEmpty()) {
        return ResponseEntity.status(404).body(Map.of(
            "status", "error",
            "error_code", "PRODUCT_NOT_FOUND",
            "message", "Produit non trouvé"
        ));
    }
    
    Product product = productOpt.get();
    BigDecimal oldPrice = product.getPrice();
    product.setPrice(request.getPrice());
    product.setUpdatedAt(LocalDateTime.now());
    
    productRepository.save(product);
    
    return ResponseEntity.ok(Map.of(
        "status", "success",
        "message", "Prix mis à jour avec succès",
        "data", Map.of(
            "server_id", product.getId(),
            "barcode", product.getBarcode(),
            "old_price", oldPrice,
            "new_price", product.getPrice(),
            "updated_at", product.getUpdatedAt().toString()
        )
    ));
}
# Python - PUT ou POST /api/products/{barcode}/price
@app.route('/api/products//price', methods=['PUT', 'POST'])
def update_price(barcode):
    data = request.get_json()
    
    if 'price' not in data or data['price'] < 0:
        return jsonify({
            'status': 'error',
            'error_code': 'INVALID_PRICE',
            'message': 'Le prix doit être un nombre positif'
        }), 400
    
    product = Product.query.filter_by(barcode=barcode).first()
    
    if not product:
        return jsonify({
            'status': 'error',
            'error_code': 'PRODUCT_NOT_FOUND',
            'message': 'Produit non trouvé'
        }), 404
    
    old_price = product.price
    product.price = data['price']
    product.updated_at = datetime.utcnow()
    
    db.session.commit()
    
    return jsonify({
        'status': 'success',
        'message': 'Prix mis à jour avec succès',
        'data': {
            'server_id': product.id,
            'barcode': product.barcode,
            'old_price': float(old_price),
            'new_price': float(product.price),
            'updated_at': product.updated_at.isoformat() + 'Z'
        }
    })
// Delphi - PUT ou POST /api/products/{barcode}/price
procedure UpdatePrice(const Barcode: string; NewPrice: Currency; ServerID: Integer);
var
  HttpClient: TNetHTTPClient;
  Response: IHTTPResponse;
  RequestBody: TJSONObject;
  ResponseJSON: TJSONObject;
begin
  HttpClient := TNetHTTPClient.Create(nil);
  try
    RequestBody := TJSONObject.Create;
    try
      RequestBody.AddPair('barcode', Barcode);
      RequestBody.AddPair('price', TJSONNumber.Create(NewPrice));
      RequestBody.AddPair('server_id', TJSONNumber.Create(ServerID));
      
      HttpClient.ContentType := 'application/json';
      HttpClient.Accept := 'application/json';
      
      Response := HttpClient.Put(
        Format('https://api.example.com/api/products/%s/price', [Barcode]),
        TStringStream.Create(RequestBody.ToString, TEncoding.UTF8)
      );
      
      if Response.StatusCode = 200 then
      begin
        ResponseJSON := TJSONObject.ParseJSONValue(Response.ContentAsString) as TJSONObject;
        try
          ShowMessage('Prix mis à jour avec succès');
        finally
          ResponseJSON.Free;
        end;
      end;
    finally
      RequestBody.Free;
    end;
  finally
    HttpClient.Free;
  end;
end;
// WinDev - PUT ou POST /api/products/{barcode}/price
PROCEDURE MettreAJourPrix(sBarcode est une chaîne, mNouveauPrix est un monétaire, nServerID est un entier) : booléen
    vRequete est un Variant
    vRequete.barcode = sBarcode
    vRequete.price = mNouveauPrix
    vRequete.server_id = nServerID
    
    sJSONRequete = VariantVersJSON(vRequete)
    
    requeteHTTP est un httpRequête
    requeteHTTP.URL = gsURLAPI + "/api/products/" + sBarcode + "/price"
    requeteHTTP.Méthode = httpPut  // ou httpPost
    requeteHTTP.Contenu = sJSONRequete
    
    tabEntetes est un tableau associatif de chaînes = ObtenirEntetes()
    POUR TOUT sValeur, sCle DE tabEntetes
        requeteHTTP.Entête[sCle] = sValeur
    FIN
    
    réponseHTTP est un httpRéponse = HTTPEnvoie(requeteHTTP)
    
    SI réponseHTTP.CodeEtat = 200 ALORS
        vRéponse est un Variant = JSONVersVariant(réponseHTTP.Contenu)
        SI vRéponse.status = "success" ALORS
            Info("Prix mis à jour", "Ancien: " + vRéponse.data.old_price, "Nouveau: " + vRéponse.data.new_price)
            RENVOYER Vrai
        FIN
    FIN
    
    RENVOYER Faux
FIN

⚡ Push API — Mise à Jour des Prix en Temps Réel

Ce point de terminaison permet aux systèmes de vente (POS) d'envoyer directement les modifications de prix au serveur IzyScan, garantissant que les afficheurs de prix reflètent immédiatement les changements sans attendre la synchronisation périodique.

⚠️ Activation Requise — Configuration Préalable
Capture d'écran — Activation du Push API dans les paramètres IzyScan Server

Paramètres IzyScan Server — Section Push API avec authentification par token

Avant de pouvoir utiliser ce point de terminaison, vous devez activer le Push API dans les paramètres du serveur IzyScan :

1 Ouvrir les Paramètres de l'application IzyScan Server
2 Aller dans l'onglet « خادم IzyScan » (IzyScan Server)
3 Défiler jusqu'à la section « Push API »
4 Activer « Push API - تحديث الأسعار الفوري »
5 (Optionnel) Activer « مصادقة بمفتاح أمان » pour sécuriser avec un token
6 Cliquer sur 💾 Sauvegarder en haut à droite

🔑 Note sur le token : Si l'authentification est activée, cliquez sur « إنشاء مفتاح أمان » pour générer un token, puis utilisez le bouton « Copy » pour le copier. Transmettez ce token au développeur du logiciel de vente pour qu'il l'inclue dans l'en-tête Authorization: Bearer <token> de ses requêtes API.

🚨 Erreur Critique à Éviter — Unités de Prix et Sémantique de promo_price

Une intégration mal configurée peut pousser des prix totalement faux vers tous les afficheurs et l'application IzyScan, pour l'intégralité du catalogue. Lisez attentivement les règles ci-dessous avant votre premier envoi.

1. Unité du prix — jamais de centimes, jamais de facteur d'échelle

Les champs price, promo_price et p_price doivent toujours être exprimés dans l'unité monétaire principale du magasin (par exemple des dinars, ex. 120.00). Ne multipliez jamais le prix par 100 (centimes), par 1 000 000, ni par aucun autre facteur avant de l'envoyer.

✅ Correct — produit à 120,00 DZD

{
  "barcode": "6130001234567",
  "price": 120.00
}

❌ Incorrect — prix multiplié par 1 000 000

{
  "barcode": "6130001234567",
  "price": 120000000.00
}
⚠️ Incident réel: une intégration a envoyé 11 846 produits avec chaque prix multiplié par 1 000 000 (ex.: "price": 120000000.00 pour un produit à 120,00 DZD en rayon). Résultat: tous les afficheurs et l'application IzyScan ont affiché un prix faux en production jusqu'à correction manuelle.
2. promo_price — jamais une copie de price

promo_price représente le prix réduit affiché pendant une promotion. Il doit valoir 0 lorsque le produit n'est pas en promotion. Ne copiez jamais la valeur de price dans promo_price: un promo_price égal à price fait apparaître le produit comme en promotion permanente sur tous les afficheurs.

✅ Correct — pas de promotion en cours

{
  "price": 120.00,
  "promo_price": 0
}

❌ Incorrect — promo_price dupliqué de price

{
  "price": 120.00,
  "promo_price": 120.00
}
3. Types acceptés et format décimal

Un nombre JSON (120.00) est le format préféré. Une chaîne numérique ("120.00") est acceptée pour compatibilité mais déconseillée. Le séparateur décimal est toujours le point (.) — jamais une virgule (,) ni un séparateur de milliers.

4. Vérifiez toujours après un premier envoi massif

Après une synchronisation initiale complète (ou tout envoi en masse), appelez GET /get?barcode=<un_code> sur quelques produits au hasard et comparez le champ price retourné au prix réellement affiché en rayon. Cette simple vérification aurait détecté l'incident ci-dessus immédiatement, avant qu'il n'affecte les 11 846 produits du catalogue.

POST /push_prices

Envoyer des Mises à Jour de Prix

Objectif: Pousser un ou plusieurs changements de prix vers IzyScan pour une mise à jour instantanée sur les afficheurs

En-têtes:
En-tête Valeur Requis
Content-Type application/json Oui
Authorization Bearer <votre_token> Uniquement si un token est configuré
Corps de la Requête — Un seul produit:
{
  "barcode": "6281001210016",
  "name": "Nom du produit",
  "price": 12.50,
  "promo_price": 9.90
}
Corps de la Requête — Plusieurs produits:
{
  "products": [
    {
      "barcode": "6281001210016",
      "name": "Lait Entier 1L",
      "price": 12.50,
      "promo_price": 9.90
    },
    {
      "barcode": "5449000000996",
      "name": "Coca-Cola 330ml",
      "price": 3.250,
      "promo_price": 0
    }
  ]
}
Champs du Produit:
Champ Type Requis Description
barcode string Oui Code-barres unique du produit (EAN-13, UPC, etc.)
name string Oui* Nom du produit. *Requis uniquement si le produit n'existe pas encore dans IzyScan
price number Oui Prix de vente actuel (≥ 0)
promo_price number Non Prix promotionnel. Si 0 ou égal au price → pas de promotion
Réponse — Tous les produits traités avec succès (200):
{
  "status": "success",
  "total": 1500,
  "updated": 1350,
  "created": 150,
  "failed": 0
}
Réponse — Avec des erreurs partielles (200):

Le traitement ne s'arrête jamais à la première erreur. Tous les produits valides sont enregistrés, et seuls les codes-barres en erreur sont listés dans failed_items.

{
  "status": "success",
  "total": 1500,
  "updated": 1347,
  "created": 148,
  "failed": 5,
  "failed_items": [
    { "barcode": "999000111", "reason": "MISSING_NAME" },
    { "barcode": "888000222", "reason": "INVALID_PRICE" },
    { "barcode": "?", "reason": "MISSING_BARCODE" }
  ]
}
Codes d'erreur par produit (reason):
Code Description
MISSING_BARCODELe champ barcode est absent ou vide
MISSING_PRICELe champ price est absent
INVALID_PRICELe prix n'est pas un nombre valide ou est négatif
MISSING_NAMELe nom est requis pour un nouveau produit (inexistant dans IzyScan)
INVALID_ITEML'élément n'est pas un objet JSON valide
WRITE_ERRORErreur d'écriture dans la base de données locale
💡 Synchronisation complète via Push API: Ce endpoint est conçu pour supporter l'envoi de tous vos produits en un seul appel (des milliers de produits). La réponse reste légère — pas de détail par produit réussi. Le traitement ne s'interrompt jamais : les produits valides sont toujours enregistrés même si certains échouent.
Réponses d'Erreur HTTP:
Code HTTP Erreur Description
401 UNAUTHORIZED Token d'authentification invalide ou manquant
405 METHOD_NOT_ALLOWED Seule la méthode POST est acceptée
400 INVALID_JSON Le corps de la requête n'est pas du JSON valide
400 EMPTY_BODY Le corps de la requête est vide
400 BATCH_TOO_LARGE Plus de 5 000 produits envoyés — paginez vos requêtes
503 PUSH_API_DISABLED Le Push API n'est pas activé dans les paramètres

📝 Exemples d'Intégration

// PHP — Envoyer une modification de prix à IzyScan
function pushPriceUpdate($izyscanHost, $port, $token, $products) {
    $url = "http://{$izyscanHost}:{$port}/push_prices";

    $headers = ['Content-Type: application/json'];
    if (!empty($token)) {
        $headers[] = "Authorization: Bearer {$token}";
    }

    $data = json_encode(['products' => $products]);

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    return ['status' => $httpCode, 'body' => json_decode($response, true)];
}

// Utilisation — lors d'un changement de prix dans votre logiciel
$result = pushPriceUpdate('192.168.1.100', 8000, 'votre_token', [
    [
        'barcode'     => '6281001210016',
        'name'        => 'Lait Entier 1L',
        'price'       => 12.50,
        'promo_price' => 9.90,
    ],
]);
// Node.js — Envoyer une modification de prix à IzyScan
async function pushPriceUpdate(izyscanHost, port, token, products) {
    const url = `http://${izyscanHost}:${port}/push_prices`;

    const headers = { 'Content-Type': 'application/json' };
    if (token) {
        headers['Authorization'] = `Bearer ${token}`;
    }

    const response = await fetch(url, {
        method: 'POST',
        headers,
        body: JSON.stringify({ products }),
    });

    return await response.json();
}

// Utilisation — Appeler cette fonction à chaque modification de prix
const result = await pushPriceUpdate('192.168.1.100', 8000, 'votre_token', [
    {
        barcode: '6281001210016',
        name: 'Lait Entier 1L',
        price: 12.50,
        promo_price: 9.90,
    },
]);
console.log(`Résultat: ${result.summary.updated} mis à jour, ${result.summary.created} créés`);
// C# — Envoyer une modification de prix à IzyScan
public async Task<string> PushPriceUpdate(
    string izyscanHost, int port, string token, object[] products)
{
    using var client = new HttpClient();
    client.Timeout = TimeSpan.FromSeconds(10);

    if (!string.IsNullOrEmpty(token))
        client.DefaultRequestHeaders.Authorization =
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);

    var payload = new { products };
    var json = System.Text.Json.JsonSerializer.Serialize(payload);
    var content = new StringContent(json, Encoding.UTF8, "application/json");

    var response = await client.PostAsync(
        $"http://{izyscanHost}:{port}/push_prices", content);

    return await response.Content.ReadAsStringAsync();
}

// Utilisation — dans votre gestionnaire d'événement de changement de prix
var result = await PushPriceUpdate("192.168.1.100", 8000, "votre_token",
    new object[] {
        new { barcode = "6281001210016", name = "Lait Entier 1L",
              price = 12.50, promo_price = 9.90 }
    });
// Java — Envoyer une modification de prix à IzyScan
import java.net.http.*;
import java.net.URI;

public String pushPriceUpdate(
        String host, int port, String token, String jsonProducts)
        throws Exception {

    HttpClient client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(10))
        .build();

    HttpRequest.Builder builder = HttpRequest.newBuilder()
        .uri(URI.create("http://" + host + ":" + port + "/push_prices"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(jsonProducts));

    if (token != null && !token.isEmpty()) {
        builder.header("Authorization", "Bearer " + token);
    }

    HttpResponse<String> response =
        client.send(builder.build(), HttpResponse.BodyHandlers.ofString());

    return response.body();
}

// Utilisation
String json = "{\"products\":[{\"barcode\":\"6281001210016\","
    + "\"name\":\"Lait Entier 1L\",\"price\":12.5,\"promo_price\":9.9}]}";
String result = pushPriceUpdate("192.168.1.100", 8000, "votre_token", json);
# Python — Envoyer une modification de prix à IzyScan
import requests

def push_price_update(izyscan_host, port, token, products):
    url = f"http://{izyscan_host}:{port}/push_prices"

    headers = {"Content-Type": "application/json"}
    if token:
        headers["Authorization"] = f"Bearer {token}"

    response = requests.post(
        url,
        json={"products": products},
        headers=headers,
        timeout=10,
    )
    response.raise_for_status()
    return response.json()

# Utilisation — à appeler à chaque changement de prix dans votre système
result = push_price_update("192.168.1.100", 8000, "votre_token", [
    {
        "barcode": "6281001210016",
        "name": "Lait Entier 1L",
        "price": 12.50,
        "promo_price": 9.90,
    },
])
print(f"Mis à jour: {result['summary']['updated']}, Créés: {result['summary']['created']}")
// Delphi — Envoyer une modification de prix à IzyScan
uses System.Net.HttpClient, System.JSON;

function PushPriceUpdate(const AIzyScanHost: string; APort: Integer;
  const AToken: string; AProducts: TJSONArray): TJSONObject;
var
  LClient: THTTPClient;
  LResponse: IHTTPResponse;
  LPayload: TJSONObject;
  LContent: TStringStream;
begin
  LClient := THTTPClient.Create;
  try
    LClient.ConnectionTimeout := 10000;
    LClient.ContentType := 'application/json';

    if AToken <> '' then
      LClient.CustomHeaders['Authorization'] := 'Bearer ' + AToken;

    LPayload := TJSONObject.Create;
    LPayload.AddPair('products', AProducts);

    LContent := TStringStream.Create(LPayload.ToJSON, TEncoding.UTF8);
    try
      LResponse := LClient.Post(
        Format('http://%s:%d/push_prices', [AIzyScanHost, APort]),
        LContent);

      Result := TJSONObject.ParseJSONValue(
        LResponse.ContentAsString) as TJSONObject;
    finally
      LContent.Free;
      LPayload.Free;
    end;
  finally
    LClient.Free;
  end;
end;

// Utilisation
var Products := TJSONArray.Create;
var Product := TJSONObject.Create;
Product.AddPair('barcode', '6281001210016');
Product.AddPair('name', 'Lait Entier 1L');
Product.AddPair('price', TJSONNumber.Create(12.50));
Product.AddPair('promo_price', TJSONNumber.Create(9.90));
Products.Add(Product);

var Result := PushPriceUpdate('192.168.1.100', 8000, 'votre_token', Products);
// WinDev — Envoyer une modification de prix à IzyScan
PROCÉDURE PushPriceUpdate(sHôte est une chaîne,
    nPort est un entier, sToken est une chaîne,
    tabProduits est un tableau de STProductPush)

    sURL est une chaîne = ChaîneConstruit(
        "http://%1:%2/push_prices", sHôte, nPort)

    // Construire le JSON
    vPayload est un Variant
    vPayload.products = tabProduits
    sJSON est une chaîne = VariantVersJSON(vPayload)

    // Préparer la requête
    cRequête est un httpRequête
    cRequête.URL = sURL
    cRequête.Méthode = httpPost
    cRequête.ContentType = "application/json"
    cRequête.Contenu = sJSON
    cRequête.Timeout = 10s

    SI sToken <> "" ALORS
        cRequête.Entête["Authorization"] = "Bearer " + sToken
    FIN

    // Envoyer
    cRéponse est un httpRéponse = HTTPEnvoie(cRequête)

    SI cRéponse.CodeEtat = 200 ALORS
        Info("Prix mis à jour avec succès")
    SINON
        Erreur("Échec: " + cRéponse.Contenu)
    FIN

FIN

// Utilisation — appeler lors de la modification d'un prix
stProduit est un STProductPush
stProduit.barcode = "6281001210016"
stProduit.name = "Lait Entier 1L"
stProduit.price = 12.50
stProduit.promo_price = 9.90

tabProduits est un tableau de STProductPush
Ajoute(tabProduits, stProduit)

PushPriceUpdate("192.168.1.100", 8000, "votre_token", tabProduits)

📘 Bonnes Pratiques d'Intégration

1. Mise à jour en temps réel (cas principal)

Appelez ce endpoint immédiatement après chaque modification de prix dans votre logiciel de vente. C'est l'usage principal du Push API — garantir que les afficheurs reflètent le bon prix en quelques secondes.


2. Synchronisation complète (full sync) — Pagination obligatoire

Ce endpoint peut aussi servir de synchronisation complète pour envoyer l'intégralité de votre catalogue. Le serveur impose une limite de 5 000 produits par requête. Au-delà, vous recevez l'erreur BATCH_TOO_LARGE.

Pour un catalogue de 50 000 produits, découpez en lots :

// Pseudo-code — Synchronisation complète paginée

BATCH_SIZE = 5000
allProducts = getAllProductsFromPOS()
totalBatches = ceil(allProducts.length / BATCH_SIZE)

// Envoyer chaque lot séquentiellement
for i = 0 to totalBatches - 1:
    batch = allProducts[i * BATCH_SIZE .. (i+1) * BATCH_SIZE]

    response = HTTP_POST("/push_prices", { "products": batch })

    if response.status == "success":
        print("Lot " + (i+1) + "/" + totalBatches +
              " — " + response.updated + " mis à jour, " +
              response.created + " créés")
    else:
        print("Erreur lot " + (i+1) + ": " + response.error)
        // Retry ce lot ou continuer selon votre logique

    // Pause optionnelle entre les lots (100-500ms)
    sleep(200ms)
Taille catalogue Lots (à 5 000/lot) Temps estimé
5 000 produits1 requête~2s
20 000 produits4 requêtes~10s
100 000 produits20 requêtes~45s

3. Règles générales
  • Retry en cas d'échec réseau — Le serveur IzyScan peut être temporairement injoignable. Réessayez 2-3 fois avec un délai croissant (1s, 3s, 5s).
  • Token en configuration — Stockez le token dans la configuration de votre logiciel, jamais en dur dans le code source.
  • Promo automatique — Si promo_price est 0 ou ≥ price, la promotion est automatiquement désactivée.
  • Nom facultatif pour les mises à jour — Le champ name n'est obligatoire que pour les nouveaux produits. Pour une mise à jour de prix, barcode + price suffisent.
  • Pas de doublons nécessaires — Si vous envoyez le même barcode deux fois dans un lot, le dernier prix l'emporte.

📥 Webhook — Notification des Changements de Prix (IzyScan → POS)

Ce point de terminaison est le miroir du Push API décrit ci-dessus, mais dans le sens opposé. Le Push API sert à envoyer des prix de votre système de vente vers IzyScan. Ce webhook sert, lui, à recevoir dans votre système de vente les changements de prix effectués depuis IzyScan.

IzyScan permet désormais à un employé de magasin de modifier un prix de vente ou un prix d'achat directement sur un terminal PDA (après saisie d'un code PIN administrateur) ou dans l'application de bureau IzyScan. Or, pour le prix de vente, IzyScan n'est pas le système de référence — votre base de données POS l'est. Sans ce webhook, la prochaine synchronisation depuis votre POS écrase silencieusement la modification faite par l'employé, qui est alors perdue.

À VENIR — Spécification Non Encore Implémentée

Cette section décrit une spécification publiée à l'avance afin que les éditeurs de logiciels de vente puissent développer et tester leur point de terminaison de réception. IzyScan n'émet pas encore ces appels aujourd'hui.

📌 En attendant, que se passe-t-il ? Sur les magasins synchronisés depuis une base de données POS, une modification de prix de vente effectuée sur un PDA ou dans l'application de bureau IzyScan est écrasée dès la prochaine synchronisation depuis le POS. En revanche, les modifications de prix d'achat (p_price) ne sont pas affectées : IzyScan les stocke lui-même et elles survivent à la synchronisation.

POST <URL configurée dans IzyScan>

Recevoir une Notification de Changement de Prix

Objectif: Permettre à votre système de vente de recevoir, en temps quasi réel, les changements de prix effectués directement dans IzyScan (PDA ou application de bureau), afin de les répercuter dans votre base de données avant qu'une synchronisation ne les écrase.

💡 Sens de l'appel — inversé par rapport au reste de ce document: pour ce point de terminaison uniquement, c'est IzyScan qui appelle votre serveur, et non l'inverse. Vous devez donc exposer une URL HTTP(S) accessible depuis le serveur IzyScan et la renseigner dans la configuration du magasin, côté IzyScan.
En-têtes envoyés par IzyScan:
En-tête Valeur Requis
Content-Type application/json Oui
Authorization Bearer <token> Uniquement si un token est configuré pour ce magasin dans IzyScan
Corps de la Requête envoyée par IzyScan:
{
  "event": "price_changed",
  "store": "<nom du magasin>",
  "changed_at": "2026-07-17T09:00:00.000Z",
  "source": "pda",
  "device_id": "1CDBD49B2778",
  "products": [
    {
      "barcode": "6130001234567",
      "name": "Lait 1L",
      "price": 120.00,
      "promo_price": 0,
      "p_price": 95.00,
      "old_p_price": 90.00
    }
  ]
}
Champs de la Requête:
Champ Type Requis Description
event string Oui Toujours "price_changed"
store string Oui Nom du magasin IzyScan concerné, tel que configuré côté IzyScan
changed_at string (ISO 8601) Oui Date et heure de la modification, en UTC
source string Oui "pda" (modification faite sur un terminal, après PIN root) ou "desktop" (modification faite dans l'application de bureau IzyScan)
device_id string Non Identifiant du terminal PDA à l'origine du changement. Absent ou non pertinent lorsque source vaut "desktop"
products array Oui Un ou plusieurs produits modifiés dans le même événement (voir ci-dessous)
Champs d'un élément de products:
Champ Type Requis Description
barcode string Oui Code-barres du produit modifié
name string Non Nom du produit, fourni à titre indicatif pour faciliter le rapprochement côté POS
price number Non Nouveau prix de vente. Présent uniquement si le prix de vente a été modifié
promo_price number Non Nouveau prix promotionnel. Présent uniquement si le prix promo a été modifié. 0 signifie que la promotion a été retirée
p_price number Non Nouveau prix d'achat (coût). Présent uniquement si le prix d'achat a été modifié
old_p_price number Non Ancien prix d'achat, fourni à titre de référence/traçabilité lorsque p_price est présent
⚠️ Un champ absent = un champ non modifié. Un payload peut ne porter qu'un seul des deux côtés (vente ou achat). Si price/promo_price sont absents, ne touchez pas au prix de vente de ce produit dans votre base — seul le prix d'achat a changé (et inversement). Ne réinitialisez jamais un champ absent à 0 ou à null.
Traitement par lots:

Plusieurs produits modifiés dans un court intervalle peuvent être regroupés dans le tableau products d'un seul appel. Votre point de terminaison doit être capable de traiter une liste, pas seulement un produit unique.

Réponse Attendue de Votre Serveur:
Code HTTP Signification pour IzyScan
200 / 201 Notification acceptée. IzyScan considère les produits du lot comme synchronisés
Tout autre code, ou timeout Échec. IzyScan réessaie avec un délai croissant et conserve les produits concernés comme en attente jusqu'à confirmation
🔁 Idempotence obligatoire: à cause des tentatives automatiques (retry) en cas d'échec ou de timeout, le même changement peut être reçu plusieurs fois par votre point de terminaison. Votre implémentation doit être idempotente: appliquer deux fois la même notification (même barcode, même changed_at) doit produire le même résultat qu'une seule application, sans effet de bord (ex.: ne pas décrémenter un historique de prix deux fois).

🧾 Synchronisation des Commandes (IzyScan → POS)

IzyScan permet à un employé de créer des commandes de vente (au comptoir, sur PDA) et des commandes d'achat (réception fournisseur, sur PDA) directement depuis le terminal, sans passer par votre logiciel de caisse. Cette section décrit les deux points de terminaison que vous devez exposer pour qu'IzyScan puisse vous transmettre ces commandes dès leur création, afin qu'elles soient enregistrées dans votre base de données au même titre qu'une commande saisie directement dans votre logiciel.

💡 Sens de l'appel — comme le webhook de prix ci-dessus: pour les deux points de terminaison décrits dans cette section, c'est IzyScan qui appelle votre serveur, et non l'inverse. Vous devez exposer deux URL HTTP(S) accessibles depuis le serveur IzyScan (une pour les ventes, une pour les achats) et les renseigner dans la configuration du magasin, côté IzyScan.
À VENIR — Spécification Non Encore Implémentée

Cette section décrit une spécification publiée à l'avance afin que les éditeurs de logiciels de vente puissent développer et tester leurs points de terminaison de réception. IzyScan n'émet pas encore ces appels aujourd'hui.

📌 Concerne uniquement le backend « Standard » (REST) : cette section s'applique aux magasins dont le backend IzyScan est de type Standard, c'est-à-dire synchronisé via les points de terminaison REST décrits dans ce document. Pour les magasins dont le backend est Odoo, la synchronisation des commandes est prévue pour une mise à jour ultérieure : IzyScan écrira alors directement les sale.order et purchase.order via le RPC d'Odoo, et un intégrateur Odoo n'aura rien à implémenter de ce qui suit.

1. Commandes de Vente (Sales Orders)

POST <URL ventes configurée dans IzyScan>

Recevoir une Commande de Vente

Objectif: Recevoir, dès sa création sur un terminal PDA, une commande de vente encaissée en magasin, afin de l'enregistrer dans votre base de données au même titre qu'une vente saisie directement dans votre logiciel de caisse.

En-têtes envoyés par IzyScan:
En-tête Valeur Requis
Content-Type application/json Oui
Authorization Bearer <token> Uniquement si un token est configuré pour ce magasin dans IzyScan
Corps de la Requête envoyée par IzyScan:
{
  "idempotency_key": "sale_1CDBD49B2778_7",
  "device_id": "1CDBD49B2778",
  "device_name": "PDA Caisse 1",
  "store": "Nom du magasin",
  "ticket_id": 7,
  "timestamp": "2026-07-17T09:00:00.000Z",
  "subtotal": 240.00,
  "total": 240.00,
  "cash_received": 250.00,
  "change_given": 10.00,
  "payment": "cash",
  "status": "done",
  "items": [
    {
      "barcode": "6130001234567",
      "name": "Lait 1L",
      "unit_price": 120.00,
      "quantity": 2,
      "line_total": 240.00
    }
  ]
}
Champs de la Requête:
Champ Type Requis Description
idempotency_key string Oui Identifiant unique et permanent de cette commande, au format "sale_{device_id}_{ticket_id}". Voir Idempotence obligatoire ci-dessous
device_id string Oui Identifiant du terminal PDA à l'origine de la vente
device_name string Non Nom convivial du terminal tel que configuré dans IzyScan, fourni à titre indicatif
store string Oui Nom du magasin IzyScan concerné, tel que configuré côté IzyScan
ticket_id number Oui Numéro de ticket de caisse, propre au terminal (device_id) — c'est la combinaison des deux qui forme un identifiant unique, via idempotency_key
timestamp string (ISO 8601) Oui Date et heure de la vente, en UTC
subtotal number Oui Montant total avant arrondi/ajustement éventuel. Même unité que le reste de ce document — voir Push API
total number Oui Montant total réellement encaissé
cash_received number Non Montant remis par le client en espèces. Présent uniquement lorsque payment vaut "cash" ou "mixed"
change_given number Non Monnaie rendue au client. Présent uniquement lorsque payment vaut "cash" ou "mixed"
payment string Oui Mode de paiement: "cash" | "card" | "mixed"
status string Oui Statut de la commande: "done" | "draft"
items array Oui Lignes de la vente (voir ci-dessous)
Champs d'un élément de items (vente):
Champ Type Requis Description
barcode string Oui Code-barres du produit vendu
name string Non Nom du produit, fourni à titre indicatif pour faciliter le rapprochement côté POS
unit_price number Oui Prix de vente unitaire au moment de la vente
quantity number Oui Quantité vendue
line_total number Oui unit_price × quantity

2. Commandes d'Achat (Purchase Orders)

POST <URL achats configurée dans IzyScan>

Recevoir une Commande d'Achat

Objectif: Recevoir, dès sa création sur un terminal PDA, une commande d'achat (réception fournisseur) saisie en magasin, afin de l'enregistrer dans votre base de données au même titre qu'une commande d'achat saisie directement dans votre logiciel.

En-têtes envoyés par IzyScan:
En-tête Valeur Requis
Content-Type application/json Oui
Authorization Bearer <token> Uniquement si un token est configuré pour ce magasin dans IzyScan
Corps de la Requête envoyée par IzyScan:
{
  "idempotency_key": "purchase_1CDBD49B2778_7",
  "device_id": "1CDBD49B2778",
  "device_name": "PDA Reception",
  "store": "Nom du magasin",
  "po_id": 7,
  "timestamp": "2026-07-17T09:00:00.000Z",
  "supplier_id": 1,
  "supplier_name": "Fournisseur A",
  "total": 1900.00,
  "item_count": 1,
  "status": "done",
  "items": [
    {
      "barcode": "6130001234567",
      "name": "Lait 1L",
      "p_price": 95.00,
      "old_p_price": 90.00,
      "quantity": 20,
      "line_total": 1900.00
    }
  ]
}
Champs de la Requête:
Champ Type Requis Description
idempotency_key string Oui Identifiant unique et permanent de cette commande, au format "purchase_{device_id}_{po_id}". Voir Idempotence obligatoire ci-dessous
device_id string Oui Identifiant du terminal PDA à l'origine de la commande
device_name string Non Nom convivial du terminal tel que configuré dans IzyScan, fourni à titre indicatif
store string Oui Nom du magasin IzyScan concerné, tel que configuré côté IzyScan
po_id number Oui Numéro de bon de commande d'achat, propre au terminal (device_id) — c'est la combinaison des deux qui forme un identifiant unique, via idempotency_key
timestamp string (ISO 8601) Oui Date et heure de la commande, en UTC
supplier_id number Non Peut être absent ou null: le PDA permet à l'opérateur de saisir un nouveau nom de fournisseur à la volée. Dans ce cas, seul supplier_name est envoyé — votre logiciel doit alors rapprocher ou créer le fournisseur par son nom
supplier_name string Oui Nom du fournisseur. Toujours présent, y compris lorsque supplier_id est absent
total number Oui Montant total de la commande
item_count number Oui Nombre de lignes/articles distincts dans la commande
status string Oui Statut de la commande: "done" | "draft"
items array Oui Lignes de la commande (voir ci-dessous)
Champs d'un élément de items (achat):
Champ Type Requis Description
barcode string Oui Code-barres du produit commandé/reçu
name string Non Nom du produit, fourni à titre indicatif pour faciliter le rapprochement côté POS
p_price number Oui Nouveau prix d'achat unitaire
old_p_price number Non Ancien prix d'achat, fourni à titre informatif pour comparaison/reporting côté POS
quantity number Oui Quantité commandée/reçue
line_total number Oui p_price × quantity

3. Comportement Commun aux Deux Points de Terminaison

📦 Un seul ordre par appel: chaque commande de vente ou d'achat est envoyée dans son propre appel HTTP, jamais regroupée avec d'autres. Ainsi, une commande en erreur ne bloque jamais le traitement des autres.
💰 Unités monétaires: tous les montants (subtotal, total, cash_received, change_given, unit_price, line_total, p_price, old_p_price) suivent la même convention que le reste de ce document: unité monétaire principale du magasin (ex. dinars, 120.00), jamais multipliée par un facteur d'échelle, séparateur décimal .. Voir les règles détaillées dans la section Push API ci-dessus.
Réponse Attendue de Votre Serveur:
Code HTTP Signification pour IzyScan
200 / 201 Commande acceptée. Vous pouvez optionnellement retourner {"id": "<votre identifiant>"} dans le corps de la réponse — IzyScan le stocke et l'affiche à côté de la commande, pour traçabilité
409 Conflict « J'ai déjà cette commande ». IzyScan traite le 409 comme un succès, pas comme une erreur. C'est la façon propre de répondre à une nouvelle tentative d'une commande déjà enregistrée
Tout autre code, ou timeout Échec. IzyScan marque la commande failed, enregistre votre message d'erreur, et réessaie avec un délai croissant (voir ci-dessous)
🔁 Idempotence obligatoire: idempotency_key identifie une commande de façon unique et définitive ("sale_{device_id}_{ticket_id}" pour une vente, "purchase_{device_id}_{po_id}" pour un achat). IzyScan réessaie systématiquement en cas d'échec, y compris lorsque votre serveur a bien enregistré la commande mais que la réponse 200 s'est perdue en chemin (coupure réseau, timeout côté client, etc.). La même commande arrivera donc plus d'une fois. Stockez idempotency_key et rejetez (avec un 409) toute commande déjà connue.

⚠️ Stockez la clé telle quelle, sans la découper. Les compteurs ticket_id (ventes) et po_id (achats) sont indépendants : un même terminal produit une vente n°7 et un achat n°7. C'est le préfixe sale_ / purchase_ qui rend la clé unique de façon globale. Si vous stockez les clés dans une table unique après avoir retiré le préfixe, vous rejetterez l'achat n°7 comme un doublon de la vente n°7 — et cette commande sera perdue silencieusement.
Réessais et Délai d'Attente:

En cas d'échec (code autre que 200/201/409, ou timeout), IzyScan réessaie automatiquement selon un délai exponentiel croissant, puis toutes les heures indéfiniment:

Tentative Délai avant la tentative suivante
11 minute
22 minutes
34 minutes
48 minutes
516 minutes
632 minutes
7 et suivantesToutes les heures
♾️ Une commande n'est jamais abandonnée: il s'agit d'enregistrements financiers. IzyScan ne renonce jamais après un nombre de tentatives donné — les réessais continuent indéfiniment. Un point de terminaison qui rejette une commande de façon permanente produit donc une commande qui réessaie pour toujours et apparaît comme en échec dans l'interface IzyScan, avec votre message d'erreur affiché à l'opérateur du magasin. Ne retournez un code d'erreur (4xx) que pour un problème réellement permanent (ex.: commande malformée), et privilégiez un message clair dans le corps de la réponse — l'opérateur du magasin le voit.
🔐 Authentification: comme pour les autres points de terminaison de ce document, l'en-tête Authorization: Bearer <token> est optionnel et n'est envoyé que si un token est configuré pour ce magasin dans IzyScan (même convention que le Push API).

📋 Points de Terminaison Inventaires

Ces points de terminaison gèrent la création et la gestion des inventaires depuis IzyScan.

POST /api/inventories

Création d'Inventaire

Objectif: Recevoir les données d'inventaire depuis IzyScan

Corps de la Requête:
{
  "name": "Nom de l'Inventaire",
  "date_created": "2024-01-15T10:30:00.000Z",
  "date_started": "2024-01-15T10:35:00.000Z",
  "date_finished": "2024-01-15T12:00:00.000Z",
  "lines": [
    {
      "barcode": "1234567890123",
      "quantity": 15.5,
      "scanned_at": "2024-01-15T10:40:00.000Z"
    }
  ]
}
Champs de l'objet Inventaire:
Champ Type Obligatoire Description
name string Obligatoire Nom de la session d'inventaire
date_created string (ISO 8601) Obligatoire Date et heure de création de l'inventaire
date_started string (ISO 8601) Optionnel Date et heure de début du scan
date_finished string (ISO 8601) Optionnel Date et heure de fin de l'inventaire
lines array Obligatoire Tableau des lignes d'inventaire
Champs de l'objet Ligne d'Inventaire:
Champ Type Obligatoire Description
barcode string Obligatoire Code-barres du produit
quantity number Obligatoire Quantité scannée
scanned_at string (ISO 8601) Obligatoire Date et heure du scan de l'article
Réponse:
{
  "status": "success",
  "message": "Inventaire créé avec succès",
  "data": {
    "inventory_id": 456,
    "lines_processed": 25,
    "total_quantity": 150.75
  }
}

Statut HTTP: 201 Created

📝 Exemples de Code

// PHP - POST /api/inventories
public function createInventory(Request $request) {
    $validated = $request->validate([
        'name' => 'required|string|max:100',
        'date_created' => 'required|date',
        'date_started' => 'nullable|date',
        'date_finished' => 'nullable|date',
        'lines' => 'required|array',
        'lines.*.barcode' => 'required|string',
        'lines.*.quantity' => 'required|numeric|min:0',
        'lines.*.scanned_at' => 'required|date'
    ]);

    DB::beginTransaction();

    try {
        $inventory = Inventory::create([
            'name' => $validated['name'],
            'date_created' => $validated['date_created'],
            'date_started' => $validated['date_started'] ?? null,
            'date_finished' => $validated['date_finished'] ?? null
        ]);

        $totalQuantity = 0;
        foreach ($validated['lines'] as $line) {
            InventoryLine::create([
                'inventory_id' => $inventory->id,
                'barcode' => $line['barcode'],
                'quantity' => $line['quantity'],
                'scanned_at' => $line['scanned_at']
            ]);
            $totalQuantity += $line['quantity'];
        }

        DB::commit();

        return response()->json([
            'status' => 'success',
            'message' => 'Inventaire créé avec succès',
            'data' => [
                'inventory_id' => $inventory->id,
                'lines_processed' => count($validated['lines']),
                'total_quantity' => $totalQuantity
            ]
        ], 201);

    } catch (\Exception $e) {
        DB::rollBack();
        return response()->json([
            'error' => 'INTERNAL_ERROR',
            'message' => 'Erreur lors de la création de l\'inventaire'
        ], 500);
    }
}
// Node.js - POST /api/inventories
app.post('/api/inventories', async (req, res) => {
  const { name, date_created, date_started, date_finished, lines } = req.body;

  // Validation
  if (!name || !date_created || !lines || !Array.isArray(lines)) {
    return res.status(400).json({
      error: 'VALIDATION_ERROR',
      message: 'Données requises manquantes'
    });
  }

  const session = await mongoose.startSession();
  session.startTransaction();

  try {
    const inventory = new Inventory({
      name,
      date_created: new Date(date_created),
      date_started: date_started ? new Date(date_started) : null,
      date_finished: date_finished ? new Date(date_finished) : null
    });

    await inventory.save({ session });

    let totalQuantity = 0;
    const inventoryLines = [];

    for (const line of lines) {
      const inventoryLine = new InventoryLine({
        inventory_id: inventory._id,
        barcode: line.barcode,
        quantity: line.quantity,
        scanned_at: new Date(line.scanned_at)
      });

      inventoryLines.push(inventoryLine);
      totalQuantity += line.quantity;
    }

    await InventoryLine.insertMany(inventoryLines, { session });

    await session.commitTransaction();

    res.status(201).json({
      status: 'success',
      message: 'Inventaire créé avec succès',
      data: {
        inventory_id: inventory._id,
        lines_processed: lines.length,
        total_quantity: totalQuantity
      }
    });

  } catch (error) {
    await session.abortTransaction();
    res.status(500).json({
      error: 'INTERNAL_ERROR',
      message: 'Erreur lors de la création de l\'inventaire'
    });
  } finally {
    session.endSession();
  }
});
// C# - POST /api/inventories
[HttpPost("inventories")]
public async Task CreateInventory([FromBody] InventoryRequest request)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(new { error = "VALIDATION_ERROR", message = "Données invalides" });
    }

    using var transaction = await _context.Database.BeginTransactionAsync();

    try
    {
        var inventory = new Inventory
        {
            Name = request.Name,
            DateCreated = request.DateCreated,
            DateStarted = request.DateStarted,
            DateFinished = request.DateFinished
        };

        _context.Inventories.Add(inventory);
        await _context.SaveChangesAsync();

        var inventoryLines = request.Lines.Select(line => new InventoryLine
        {
            InventoryId = inventory.Id,
            Barcode = line.Barcode,
            Quantity = line.Quantity,
            ScannedAt = line.ScannedAt
        }).ToList();

        _context.InventoryLines.AddRange(inventoryLines);
        await _context.SaveChangesAsync();

        await transaction.CommitAsync();

        var totalQuantity = request.Lines.Sum(l => l.Quantity);

        return StatusCode(201, new
        {
            status = "success",
            message = "Inventaire créé avec succès",
            data = new
            {
                inventory_id = inventory.Id,
                lines_processed = request.Lines.Count,
                total_quantity = totalQuantity
            }
        });
    }
    catch (Exception)
    {
        await transaction.RollbackAsync();
        return StatusCode(500, new { error = "INTERNAL_ERROR", message = "Erreur lors de la création de l'inventaire" });
    }
}
// Java - POST /api/inventories
@PostMapping("/api/inventories")
@Transactional
public ResponseEntity createInventory(@RequestBody @Valid InventoryRequest request) {
    try {
        Inventory inventory = new Inventory();
        inventory.setName(request.getName());
        inventory.setDateCreated(request.getDateCreated());
        inventory.setDateStarted(request.getDateStarted());
        inventory.setDateFinished(request.getDateFinished());

        inventory = inventoryRepository.save(inventory);

        List lines = new ArrayList<>();
        BigDecimal totalQuantity = BigDecimal.ZERO;

        for (InventoryLineRequest lineRequest : request.getLines()) {
            InventoryLine line = new InventoryLine();
            line.setInventory(inventory);
            line.setBarcode(lineRequest.getBarcode());
            line.setQuantity(lineRequest.getQuantity());
            line.setScannedAt(lineRequest.getScannedAt());

            lines.add(line);
            totalQuantity = totalQuantity.add(lineRequest.getQuantity());
        }

        inventoryLineRepository.saveAll(lines);

        return ResponseEntity.status(201).body(Map.of(
            "status", "success",
            "message", "Inventaire créé avec succès",
            "data", Map.of(
                "inventory_id", inventory.getId(),
                "lines_processed", lines.size(),
                "total_quantity", totalQuantity
            )
        ));

    } catch (Exception e) {
        return ResponseEntity.status(500).body(Map.of(
            "error", "INTERNAL_ERROR",
            "message", "Erreur lors de la création de l'inventaire"
        ));
    }
}
# Python - POST /api/inventories
@app.route('/api/inventories', methods=['POST'])
def create_inventory():
    data = request.get_json()

    # Validation
    required_fields = ['name', 'date_created', 'lines']
    for field in required_fields:
        if field not in data:
            return jsonify({
                'error': 'VALIDATION_ERROR',
                'message': f'Champ requis manquant: {field}'
            }), 400

    try:
        inventory = Inventory(
            name=data['name'],
            date_created=datetime.fromisoformat(data['date_created'].replace('Z', '+00:00')),
            date_started=datetime.fromisoformat(data['date_started'].replace('Z', '+00:00')) if data.get('date_started') else None,
            date_finished=datetime.fromisoformat(data['date_finished'].replace('Z', '+00:00')) if data.get('date_finished') else None
        )

        db.session.add(inventory)
        db.session.flush()  # Pour obtenir l'ID

        total_quantity = 0
        for line_data in data['lines']:
            line = InventoryLine(
                inventory_id=inventory.id,
                barcode=line_data['barcode'],
                quantity=line_data['quantity'],
                scanned_at=datetime.fromisoformat(line_data['scanned_at'].replace('Z', '+00:00'))
            )
            db.session.add(line)
            total_quantity += line_data['quantity']

        db.session.commit()

        return jsonify({
            'status': 'success',
            'message': 'Inventaire créé avec succès',
            'data': {
                'inventory_id': inventory.id,
                'lines_processed': len(data['lines']),
                'total_quantity': total_quantity
            }
        }), 201

    except Exception as e:
        db.session.rollback()
        return jsonify({
            'error': 'INTERNAL_ERROR',
            'message': 'Erreur lors de la création de l\'inventaire'
        }), 500
// Delphi - POST /api/inventories
procedure CreateInventory(const InventoryData: TInventoryRequest);
var
  HttpClient: TNetHTTPClient;
  Response: IHTTPResponse;
  RequestJSON, LineJSON: TJSONObject;
  LinesArray: TJSONArray;
  Line: TInventoryLine;
  ResponseJSON: TJSONObject;
begin
  HttpClient := TNetHTTPClient.Create(nil);
  try
    RequestJSON := TJSONObject.Create;
    try
      RequestJSON.AddPair('name', InventoryData.Name);
      RequestJSON.AddPair('date_created', DateTimeToISO8601(InventoryData.DateCreated));
      if InventoryData.DateStarted > 0 then
        RequestJSON.AddPair('date_started', DateTimeToISO8601(InventoryData.DateStarted));
      if InventoryData.DateFinished > 0 then
        RequestJSON.AddPair('date_finished', DateTimeToISO8601(InventoryData.DateFinished));

      LinesArray := TJSONArray.Create;
      for Line in InventoryData.Lines do
      begin
        LineJSON := TJSONObject.Create;
        LineJSON.AddPair('barcode', Line.Barcode);
        LineJSON.AddPair('quantity', TJSONNumber.Create(Line.Quantity));
        LineJSON.AddPair('scanned_at', DateTimeToISO8601(Line.ScannedAt));
        LinesArray.AddElement(LineJSON);
      end;
      RequestJSON.AddPair('lines', LinesArray);

      HttpClient.ContentType := 'application/json';
      HttpClient.Accept := 'application/json';

      Response := HttpClient.Post(
        'https://api.example.com/api/inventories',
        TStringStream.Create(RequestJSON.ToString, TEncoding.UTF8)
      );

      if Response.StatusCode = 201 then
      begin
        ResponseJSON := TJSONObject.ParseJSONValue(Response.ContentAsString) as TJSONObject;
        try
          if ResponseJSON.GetValue('status').Value = 'success' then
            ShowMessage('Inventaire créé avec succès');
        finally
          ResponseJSON.Free;
        end;
      end;
    finally
      RequestJSON.Free;
    end;
  finally
    HttpClient.Free;
  end;
end;
// WinDev - POST /api/inventories
PROCEDURE CréerInventaire(sNom est une chaîne, dtDateCreated est un DateHeure, dtDateStarted est un DateHeure = "", dtDateFinished est un DateHeure = "", tabLignes est un tableau de STInventoryLine) : entier
    vInventaire est un Variant
    vInventaire.name = sNom
    vInventaire.date_created = DateHeureVersISO8601(dtDateCreated)

    SI dtDateStarted <> "" ALORS
        vInventaire.date_started = DateHeureVersISO8601(dtDateStarted)
    FIN
    SI dtDateFinished <> "" ALORS
        vInventaire.date_finished = DateHeureVersISO8601(dtDateFinished)
    FIN

    Dimension(vInventaire.lines, 0)
    POUR TOUT stLigne DE tabLignes
        vLigne est un Variant
        vLigne.barcode = stLigne.barcode
        vLigne.quantity = stLigne.quantity
        vLigne.scanned_at = stLigne.scanned_at

        Ajoute(vInventaire.lines, vLigne)
    FIN

    sJSONRequete = VariantVersJSON(vInventaire)

    requeteHTTP est un httpRequête
    requeteHTTP.URL = gsURLAPI + "/api/inventories"
    requeteHTTP.Méthode = httpPost
    requeteHTTP.Contenu = sJSONRequete

    tabEntetes est un tableau associatif de chaînes = ObtenirEntetes()
    POUR TOUT sValeur, sCle DE tabEntetes
        requeteHTTP.Entête[sCle] = sValeur
    FIN

    réponseHTTP est un httpRéponse = HTTPEnvoie(requeteHTTP)

    SI réponseHTTP.CodeEtat = 201 ALORS
        vRéponse est un Variant = JSONVersVariant(réponseHTTP.Contenu)
        SI vRéponse.status = "success" ALORS
            Info("Inventaire créé", "ID: " + vRéponse.data.inventory_id)
            RENVOYER vRéponse.data.inventory_id
        FIN
    FIN

    RENVOYER 0
FIN

📊 Modèles de Données

Modèle Produit

Champ Type Obligatoire Description
id string/number Obligatoire Identifiant unique du produit (peut être identique au barcode)
name string Obligatoire Nom d'affichage du produit
barcode string Obligatoire Code-barres du produit (EAN-13, UPC, etc.). Les produits sans code-barres sont ignorés.
price number Obligatoire Prix de vente actuel
promo_price number Optionnel Prix promotionnel (si applicable)
server_id string/number Optionnel ID du produit côté serveur (utilisé pour les mises à jour de prix)

Modèle Inventaire

Champ Type Obligatoire Description
name string Obligatoire Nom de la session d'inventaire
date_created string (ISO 8601) Obligatoire Date et heure de création de l'inventaire
date_started string (ISO 8601) Optionnel Date et heure de début du scan
date_finished string (ISO 8601) Optionnel Date et heure de fin de l'inventaire
lines array Obligatoire Tableau des lignes d'inventaire

Modèle Ligne d'Inventaire

Champ Type Obligatoire Description
barcode string Obligatoire Code-barres du produit
quantity number Obligatoire Quantité scannée
scanned_at string (ISO 8601) Obligatoire Date et heure du scan de l'article

⚠️ Gestion des Erreurs

Format de Réponse d'Erreur Standard (Recommandé)

{
  "error": "CODE_ERREUR",
  "message": "Message d'erreur lisible"
}

Codes de Statut HTTP

Code Description
200 OK Requêtes GET/PUT réussies
201 Created Requêtes POST réussies
400 Bad Request Données de requête invalides
401 Unauthorized Authentification requise ou échouée
404 Not Found Ressource non trouvée
422 Unprocessable Entity Erreurs de validation
500 Internal Server Error Erreur serveur

Codes d'Erreur Courants

  • INVALID_CREDENTIALS - Authentification échouée
  • PRODUCT_NOT_FOUND - Produit avec le code-barres donné non trouvé
  • INVALID_BARCODE - Format du code-barres invalide
  • INVALID_PRICE - Valeur du prix invalide
  • DUPLICATE_BARCODE - Le code-barres existe déjà
  • VALIDATION_ERROR - Validation des données échouée
  • INTERNAL_ERROR - Erreur interne du serveur

🔄 Flux d'Implémentation

Cette section décrit les flux typiques d'utilisation de l'API par le client IzyScan.

1. Synchronisation Complète Initiale

  1. Le client appelle GET /api/ping pour vérifier la connectivité
  2. Le client appelle GET /api/products?limit=2000&offset=0
  3. Le client continue à récupérer avec un offset croissant jusqu'à ce que has_more soit false
  4. Le client stocke le timestamp de la dernière synchronisation localement

2. Synchronisation Incrémentale

  1. Le client appelle GET /api/products?updated_since=2024-01-15 10:30:00&limit=2000&offset=0
  2. Seuls les produits modifiés après la date donnée sont retournés
  3. Le client met à jour sa base de données locale avec les changements

3. Envoi d'Inventaire

  1. L'utilisateur termine une session d'inventaire sur l'appareil mobile
  2. Le client appelle POST /api/inventories avec les données complètes de l'inventaire
  3. Le serveur traite et stocke l'inventaire

4. Mise à Jour de Prix

  1. L'utilisateur modifie le prix sur l'appareil mobile
  2. Le client appelle PUT /api/products/{barcode}/price
  3. Le serveur met à jour le prix du produit dans le système backend
💡 Notes pour l'Implémentation:
  • Le code-barres est la clé: Le client utilise le code-barres comme identifiant principal pour les produits localement
  • Synchronisation Incrémentale: Implémenter le filtre updated_since améliore significativement les performances de synchronisation pour les grands catalogues
  • Pagination Requise: Pour les catalogues de plus de 2000 produits, la pagination est essentielle
  • Encodage UTF-8: Tous les champs texte doivent être encodés en UTF-8
  • Fuseau Horaire: Toutes les valeurs datetime doivent être au format ISO 8601 (UTC recommandé)

✨ Meilleures Pratiques

1. 🚀 Optimisation des Performances

  • Pagination: Toujours implémenter la pagination pour les listes de produits
  • Indexation: Créer des index de base de données sur les champs barcode, updated_at

2. 🔄 Cohérence des Données

  • Validation: Valider toutes les données d'entrée avant traitement
  • Transactions: Utiliser des transactions de base de données pour les opérations critiques
  • Contraintes Uniques: Assurer l'unicité des codes-barres dans votre base de données
  • Précision Décimale: Utiliser une précision décimale appropriée pour les prix

3. 🔐 Sécurité

  • Authentification: Implémenter des mécanismes d'authentification robustes
  • Sanitisation des Entrées: Sanitiser toutes les données d'entrée
  • Journalisation: Journaliser tous les accès API à des fins d'audit

4. 🧪 Tests

Liste de Vérification des Tests

  • Le point de terminaison de vérification de santé répond correctement
  • Synchronisation des produits avec divers filtres
  • Fonctionnalité de mise à jour des prix
  • Création d'inventaire avec plusieurs lignes
  • Mécanismes d'authentification
  • Gestion des erreurs pour les requêtes invalides
  • Fonctionnalité de pagination
  • Performance sous charge

Données de Test Échantillon

{
  "test_product": {
    "id": 999,
    "name": "Produit Test",
    "barcode": "9999999999999",
    "price": 10.99,
    "promo_price": 8.99,
    "server_id": 999
  },
  "test_inventory": {
    "name": "Inventaire Test",
    "date_created": "2024-01-15T10:30:00.000Z",
    "date_started": "2024-01-15T10:35:00.000Z",
    "date_finished": "2024-01-15T12:00:00.000Z",
    "lines": [
      {
        "barcode": "9999999999999",
        "quantity": 5.0,
        "scanned_at": "2024-01-15T10:40:00.000Z"
      }
    ]
  }
}

📞 Support

Pour le support technique et les questions concernant cette intégration:

  • Support d'Implémentation: Contactez l'équipe technique IzyScan
  • Email: m.benyoub@massani3.com
  • Téléphone: 00213560389077

تحتاج مثالًا كاملًا أو مرافقة تقنية؟

توثيق مفصّل، أمثلة برمجية (PHP، Node.js، C#، Java، Python، Delphi، WinDev) ودعم تقني مخصص لمطوّري أنظمة نقاط البيع.