Skip to main content

Genuka API — Guide d’intégration e-commerce

Public visé : développeurs front-end / full-stack qui construisent un site e-commerce
pour une entreprise déjà inscrite sur Genuka.
Version d’API : 2023-11 · Préfixe de toutes les routes : /2023-11

1. Objectif et périmètre

Ce guide décrit, endpoint par endpoint et payload par payload, comment bâtir une boutique en ligne au-dessus de genuka-api : catalogue · variantes · stock · collections · panier · authentification client · adresses · réductions · livraison & retrait (pickup) · création de commande · paiement · suivi de livraison · documents de commande · multi-devise · multi-boutique · contenu éditorial (blogs, articles, pages CMS) · réservation de services (créneaux) · chat storefront · factures publiques. Tous les faits ci-dessous ont été vérifiés directement dans le code routes/api/V202311/, controllers app/Http/Controllers/V202311/, pipeline app/Processes/Order/, resources app/Http/Resources/V202311/). Quand un comportement est calculé côté serveur, c’est signalé.

2. Concepts clés

2.1 Multi-tenant

Une requête storefront est toujours scopée à une entreprise Company), et de préférence à une boutique Shop). Le scoping passe par des en-têtes HTTP, pas par l’URL. | En-tête | Obligatoire | Rôle | |---|---|---| | X-Company: <company_id> | Oui pour toutes les routes storefront / customer | Identifie l’entreprise. Middleware check.company. Absent → 422 { "message": "Please provide a Company Id" }. | | X-ShopId: <shop_id> | Recommandé | Scope le catalogue, le stock, les prix et le checkout sur la boutique active. À défaut, l’API lit X-Shop. | | Authorization: Bearer <token> | Oui pour les routes client connecté auth:customer) | Jeton OAuth (Passport) renvoyé par login / register (ou par POST /customers/orders avec authenticate). | | Content-Language: fr | Optionnel | Langue des libellés / e-mails. |
Le middleware tenant.scope applique automatiquement les scopes entreprise/boutique sur
les modèles : vous ne « voyez » jamais les données d’une autre entreprise.

2.2 Le problème de l’amorçage (bootstrap)

X-Company est l’ID de l’entreprise — vous ne l’avez pas encore au tout premier appel. Pour le récupérer à partir du nom de domaine de la boutique, utilisez la route racine hors scope :

GET /2023-11/company/from-domain?domain=[ma-boutique.com](http://ma-boutique.com)
Une fois l’ID connu, placez X-Company sur toutes les requêtes suivantes.

2.3 Entités centrales

  • Company — branding, devise par défaut, devises additionnelles, moyens de paiement, config storefront
  • Shop — point de vente ; peut porter sa propre devise ; relié à des entrepôts
  • Product / ProductVariant — un produit a une ou plusieurs variantes ; le prix et le stock vivent au niveau variante
  • Stock (entrepôt) — relié aux boutiques via shop_warehouse
  • Customer, Address
  • Orderbilling (JSON), shipping (JSON), discounts, deliveries, payments, medias
  • ShippingFee, PickupLocation, Delivery, DeliveryTracking
  • PaymentMethod, Payment
  • Collection (catalogue groupé, imbricable) ; Blog / Article / Page (CMS storefront)
  • Availability / Unavailability / CalendarEvent (réservation de services par professionnel)
  • InboxConversation / Message (chat storefront)

3. Parcours storefront de référence

  1. Résoudre l’entreprise /company/from-domain) → fixer X-Company
  2. Charger les boutiques, choisir une boutique active → fixer X-ShopId
  3. Lister les produits / variantes (filtres, pagination)
  4. Calculer la disponibilité à partir des champs estimated_quantity* et follow_stock
  5. (Optionnel) Authentifier le client — le checkout invité est possible
  6. Gérer les adresses du client
  7. Vérifier un code promo / charger les promos automatiques
  8. Choisir livraison ou retrait
  9. Créer la commande POST /customers/orders)
  10. Récupérer le lien de paiement (immédiat ou différé)
  11. Suivre la commande et la livraison
Panier : Genuka ne stocke pas de panier côté serveur. Le panier est géré par le front
(local/session) jusqu’à la création de la commande.

4. Référence des endpoints

4.1 Contexte entreprise & boutique

| Méthode | Endpoint | Notes | |---|---|---| | GET | /2023-11/company/from-domain?domain=<domain> | Bootstrap, hors X-Company. Accepte aussi ?companyId=<id>. Réponse : CompanyResource public (en cache). | | GET | /2023-11/companies/{company} | Entreprise par ID, hors scope. | | GET | /2023-11/company?domain=<domain> | Variante scopée (nécessite X-Company). | | GET | /2023-11/company/filters?companyId=<id> | Filtres storefront configurés storeFrontFilters). | | GET | /2023-11/shops | Liste paginée des boutiques. | | GET | /2023-11/shops/{shop} | Détail boutique. Variante : /shops/handle/{slug}. | | GET | /2023-11/shops/count | Nombre de boutiques. |
GET /company/from-domain et GET /companies/{company} sont mis en cache 4 semaines
(clés public_company_domain_{domain} / public_company_id_{companyId}). C’est le point
d’amorçage : un seul appel par session suffit.
CompanyResource (public)* — champs exposés app/Http/Resources/V202311/Public/CompanyResource.php) : | Champ | Type | Description | |---|---|---| | id | ULID | Identifiant entreprise (= X-Company). | | name, description, type | string | Identité. | | handle, company_code | string | Slug et code public. | | logoUrl | string | URL directe du logo. | | logo | objet média | Média complet microthumblargewebp), ou null. | | storefront | objet | Config du thème actif ThemeConfiguration is_active=1), renvoyée telle quelle. | | storeFrontFilters | objet | Filtres pré-calculés (voir ci-dessous). | | paymentMethods[] | tableau | Moyens de paiement activés (voir ci-dessous). | | currency | { code, name } | Devise par défaut. | | currencies[] | { code, label, value } | Devises additionnelles + taux. | | variables | objet | Variables custom metadata.variables). | | website | objet domaine | Domaine principal { id, domain, main, is_active, … }). | | hasBlogs | bool | L’entreprise publie-t-elle un blog. | | contact | objet | Coordonnées publiques metadata.contact). | | address | AddressResource | Adresse de l’entreprise. | | business_id | string | RCCM metadata.rccm). | | tax_id | string | Identifiant fiscal metadata.fiscalID). | storeFrontFilters* (aussi via /company/filters) contient :

{

  "collections": [ { "id", "name", "slug", "medias": [...], "sub_collections": [...] } ],

  "minPrice": 1000,

  "maxPrice": 250000,

  "options": { "Couleur": ["Rouge", "Bleu"], "Taille": ["S", "M", "L"] }

}
paymentMethods[]* PaymentMethodResource) — un par moyen activé :

{

  "id": "<ulid>",

  "name": "Carte bancaire",

  "processor": "stripe",

  "status": true,

  "account_id": null,

  "treasury_account_id": null,

  "metadata": {},

  "configurations": { "publicKey": "pk_live_…" }

}
processorcash, notchpay, stripe, paydunya, taramoney, pawapay, paypal,
mamoni, flutterwave (enum App\Enum\PaymentMethods). Un type fonctionnel l’accompagne
côté config : card, mobile_money, bank_transfer App\Enum\PaymentMethodType).
Les valeurs secrètes des configurations (clés secretprivatetoken) sont masquées en sortie.
Au checkout, billing.method reprend la valeur processor du moyen choisi (§4.8).
ShopResource* — champs exposés /shops, /shops/{shop}) : | Champ | Description | |---|---| | id, name, slug, description | Identité boutique. | | currency_code, currency_name | Devise propre à la boutique (prioritaire sur l’entreprise). | | address | AddressResource (avec latitudelongitude). | | warehouses[] | Entrepôts rattachés { id, name, description }) — base du stock. | | domains[] | Domaines de la boutique { domain, main, is_active }). | | logoUrl, logo | Logo (URL + média). | | metadata, company_id | — | /shops accepte filter[search] (nom/description) et sort=name|currency_code|created_at|…. /shops/{shop} accepte aussi ?companyId= pour résoudre par couple company_idslug. Route filtres : GET /2023-11/company/filters?companyId=<id> renvoie directement le bloc storeFrontFilters (collections + bornes de prix + options) — pratique pour bâtir la facette de recherche sans charger toute la CompanyResource. Bonne pratique : fixez la boutique active très tôt et propagez X-ShopId partout (catalogue, stock, checkout, suivi).

4.2 Catalogue produit

| Méthode | Endpoint | Notes | |---|---|---| | GET | /2023-11/products | Liste paginée. | | GET | /2023-11/products/count | { "count": <int> }. | | GET | /2023-11/products/{product} | Détail ProductDetailResource). | | GET | /2023-11/products/handle/{handle} | Détail par slug (réponse en cache). | | GET | /2023-11/products/{product}/similar?limit=5 | Produits similaires. | | GET | /2023-11/products/handle/{handle}/similar | Similaires par slug. | | GET | /2023-11/productVariants | Liste de variantes paginée. | | GET | /2023-11/productVariants/{productVariant} | Détail variante. | Pagination (standard Laravel + Spatie QueryBuilder) : ?per_page=20 (défaut 15, max 100) ou ?page[size]=20&page[number]=2. La réponse liste est enveloppée : { "data": [...], "links": {...}, "meta": {...} }. Filtres disponibles sur /products filter[<nom>]=...) : | Filtre | Exemple | Effet | |---|---|---| | search | filter[search]=tshirt | Titre, handle, vendeur, titre/SKU/code-barres des variantes. | | published | filter[published]=1 | Produits publiés. | | type | filter[type]=physical | Type de produit. | | has_stocks | filter[has_stocks]=1 | Uniquement les produits disponibles. | | priceRange | filter[priceRange]=1000,5000 | Fourchette [min,max]. | | collections | filter[collections]=id1,id2 | Par collection. | | shops | filter[shops]=shop_id | Par boutique. | | tags | filter[tags]=promo,ete | Par tags. | | optionValues | filter[optionValues]=Rouge | Par valeur d’option. | | created_at / updated_at | filter[created_at]=... | Plage de dates { start, end }. | Includes ?include=...) : variants, variants.stocks, variants.warehouseQuantities, collections, shops, tags, options, supplier, service.professionals Tris ?sort=..., préfixe - pour décroissant) : price, -created_at, -updated_at, title, has_stock_first, total_orders, stocks, tags.

4.3 Stock & disponibilité

Le prix et le stock vivent au niveau variante. Les resources variantes exposent :
  • estimated_quantity — quantité estimée totale (toutes boutiques), ou null si aucune variante ne suit le stock
  • estimated_quantity_by_shop{ shop_id: quantité }
  • estimated_quantity_by_warehouse{ warehouse_id: quantité }
  • stocks — détail par entrepôt (filtré sur la boutique active si X-ShopId)
Règle métier de disponibilité :
Le suivi de stock se règle par variante via la colonne *follow_stock**.
Si follow_stock = false, la variante est toujours considérée disponible, quelle que
soit estimated_quantity. Si follow_stock = true, basez la disponibilité sur
estimated_quantity (globale ou par boutique selon X-ShopId).
Au niveau produit, ProductDetailResource expose un booléen follow_stock (vrai si au moins une variante suit le stock) et remaining_stocks (restant par entrepôt pour la boutique).
⚠️ Le stock affiché côté front est indicatif. La disponibilité est **revérifiée au moment
de la création de commande** (voir §4.8). Ne vous fiez jamais au stock client comme source de vérité.

4.4 Authentification client

Routes publiques X-Company requis, pas de token) : | Méthode | Endpoint | Champs | |---|---|---| | POST | /2023-11/customers/register | password (requis) ; first_name ou last_name ; email ou phone (uniques par entreprise). | | POST | /2023-11/customers/login | email ou phone + password. | | POST | /2023-11/customers/logout | — | | POST | /2023-11/customers/password/reset-link | email. Renvoie un token. | | POST | /2023-11/customers/password/forgot | email. | | POST | /2023-11/customers/password/reset | token, email, password (confirmé, min 8). | Réponse register / login 200) :

{

  "token": "1|XXXXXXXXXXXXXXXXXXXX",

  "customer": { "id": "...", "first_name": "Jean", "email": "[[email protected]](mailto:[email protected])" }

}
Le token est un jeton OAuth (Passport). Placez-le ensuite en Authorization: Bearer <token>. Mauvais identifiants → 401.
Connecter le client dès le checkout : vous pouvez aussi obtenir un token directement à la
création de commande, sans appel registerlogin séparé — voir le paramètre authenticate en §4.8.

4.5 Profil & adresses (client connecté)

Routes auth:customer Bearer requis) : | Méthode | Endpoint | |---|---| | GET | /2023-11/customers/profile | | PUT | /2023-11/customers/profile | | PUT | /2023-11/customers/profile/password current_password, new_password) | | GET | /2023-11/customers/addresses | | POST | /2023-11/customers/addresses | | GET | /2023-11/customers/addresses/{address} | | PUT | /2023-11/customers/addresses/{address} | | DELETE | /2023-11/customers/addresses/{address} | CustomerResource renvoie entre autres : addresses[], default_address, contacts[], tags[], medias[], custom_fields.

4.6 Livraison & retrait

| Méthode | Endpoint | Notes | |---|---|---| | GET | /2023-11/shipping-fees | Frais de livraison disponibles (paginé ; filter[active], filter[search], filter[created_at], filter[updated_at] ; sort=name, orders_count). | | GET | /2023-11/pickup-locations | Points de retrait (paginé ; filter[shop], filter[active], filter[search]). | ShippingFeeResource* — champs :

{

  "id": "<ulid>", "name": "Livraison Douala", "type": "fixed", "amount": 2000,

  "active": true, "shop_id": "<ulid>", "shop": { },

  "warehouses": [ ], "warehouse_ids": ["<ulid>"], "metadata": {},

  "created_at": "…", "updated_at": "…"

}
type vaut toujours fixed (enum ShippingFeeType) : le amount est un forfait. Le
ciblage géographique se fait par rattachement à des entrepôts warehouse_ids), pas par des
zones tarifaires côté resource. Filtrez les frais pertinents selon la boutique / l’entrepôt active(s).
PickupLocationResource* — mêmes champs de base id, name, amount, active, shop, warehouses, warehouse_ids) plus :

{

  "address": { "line1": "…", "city": "…", "country": "…", "latitude": 4.05, "longitude": 9.7, "…": "…" },

  "full_address": "Rue 1.234, Douala, CM"

}
Les coordonnées GPS du point de retrait sont dans address.latitude / address.longitude
(utiles pour une carte). Les horaires d’ouverture, s’ils existent, vivent dans metadata /
address.metadata (pas de champ dédié).
Au checkout, présentez les deux options puis renseignez la commande (voir §4.8) :
  • Livraison : shipping.mode = "delivery" + shipping_fee_id
  • Retrait : shipping.mode = "pickup" + pickup_location_id
shipping_fee_id et pickup_location_id sont mutuellement exclusifs. Le montant de
livraison est résolu côté serveur depuis le frais ou le point de retrait choisi ; à défaut,
il prend shipping.amount.

4.7 Réductions

| Méthode | Endpoint | Notes | |---|---|---| | GET | /2023-11/discounts?code=WELCOME10 | Vérifie un code. 200 + DiscountResource, ou 404 si invalide. | | POST | /2023-11/discounts/automatic | Promos automatiques applicables. Body : { "customer": { "id" } } ou { "customer": { "email", "phone" } }. | Au moment de la commande, transmettez les remises validées dans discounts[] (voir §4.8). Deux niveaux existent : order-level discounts[]) et product-level products[].discounts[]). Le moteur applique et persiste les montants.

4.8 Création de commande


POST /2023-11/customers/orders
  • Fonctionne en checkout invité (l’objet customer crée/retrouve le client) ou connecté Bearer → le client authentifié est utilisé).
  • Pilotée par le pipeline UpsertOrderProcess (résolution boutique → client → produits → remises → taxes → livraison → facturation → écriture → médias → notifications).

⚠️ Les totaux sont calculés par le serveur

ManageBillingTask recalcule subtotal, discount, out_of_tax_total, sum_positive_taxes, sum_negative_taxes, total et net_to_pay à partir des products, du shipping et des discounts. N’envoyez pas ces montants : ils seraient écrasés. Côté client, vous fournissez les lignes, le mode de paiement, le mode de livraison et les adresses — c’est tout. Formule appliquée :

subtotal          = Σ (price × quantity)

out_of_tax_total  = subtotal + shipping − discount

total = net_to_pay = out_of_tax_total + taxes_positives + taxes_negatives

Payload recommandé (minimal et correct)


{

  "source": "Genuka Checkout",

  "deferred_payment": true,

  "currency_code": "XAF",

  "customer": {

    "first_name": "Jean",

    "last_name": "Dupont",

    "email": "[[email protected]](mailto:[email protected])",

    "phone": "+237600000000",

    "force": true

  },

  "products": [

    { "product_id": "<product_ulid>", "variant_id": "<variant_ulid>", "quantity": 2, "price": 15000 }

  ],

  "billing": {

    "method": "cash",

    "status": "pending",

    "address_id": "<address_ulid>"

  },

  "shipping": {

    "mode": "delivery",

    "status": "pending",

    "scheduled_at": "2026-05-22T10:00:00Z",

    "address_id": "<address_ulid>"

  },

  "shipping_fee_id": "<shipping_fee_ulid>",

  "discounts": [

    { "discount_id": "<discount_ulid>", "code": "WELCOME10" }

  ],

  "metadata": { "note": "Livrer avant midi" }

}
Dans cet exemple : 2 × 15 000 = 30 000 de produits + le montant du shipping_fee_id.
Le total et le net_to_pay renvoyés intègrent ces frais — pas besoin de les calculer.
Notes sur les champs :
  • customer.force = true : crée le client même si un email/téléphone proche existe (utile en checkout invité). À false, un conflit lève une erreur.
  • billing.method : cash, bank, stripe, paydunya, pawapay, paypal, mamoni, taramoney
  • shipping.mode : delivery, pickup, postal, online, other.
  • address_id (existante) ou address (objet { first_name, last_name, line1, city, country, … }) pour billing/shipping.
  • Devise résolue dans l’ordre : currency_code (requête) → boutique → entreprise → XAF.
  • authenticate (booléen) : si true, un token OAuth est émis pour le client de la commande et renvoyé dans la réponse (voir « Connecter le client au checkout » ci-dessous). Pratique pour authentifier automatiquement un acheteur en checkout invité.

Livrer à un tiers (cadeau, destinataire ≠ acheteur)

L’adresse de livraison shipping.address) est indépendante de l’acheteur : elle porte ses propres first_name, last_name, email, phone. Pour expédier à quelqu’un d’autre, laissez l’acheteur dans customer (et billing) et décrivez le destinataire dans shipping.address :

{

  "customer": {

    "first_name": "Jean", "last_name": "Dupont",

    "email": "[[email protected]](mailto:[email protected])", "phone": "+237600000000", "force": true

  },

  "products": [

    { "product_id": "<product_ulid>", "variant_id": "<variant_ulid>", "quantity": 1, "price": 15000 }

  ],

  "billing": { "method": "cash", "status": "pending" },

  "shipping": {

    "mode": "delivery",

    "status": "pending",

    "is_new": true,

    "address": {

      "first_name": "Marie", "last_name": "Kamga",

      "phone": "+237699999999", "email": "[[email protected]](mailto:[email protected])",

      "line1": "Rue 1.234, Bonapriso", "city": "Douala", "country": "CM"

    }

  },

  "shipping_fee_id": "<shipping_fee_ulid>"

}
Pièges à connaître (cf. ManageShippingTask) :
  1. line1, city, country sont obligatoires dans shipping.address, sinon 400.
  2. Mettez "is_new": true* : sinon, pour un client connecté, le serveur réutilise une adresse existante qui matche line1citycountry. is_new force la création d’une nouvelle adresse de livraison.
  3. N’utilisez pas un shipping.address_id existant de l’acheteur pour livrer ailleurs : passer un address_id avec un objet address écrase cette adresse. Pour un tiers, envoyez toujours un objet address neuf.
  4. L’adresse est enregistrée avec is_shipping = true, is_billing = false. Comme un nom de destinataire est fourni, elle ne devient pas l’adresse primaire de l’acheteur is_primary reste false) — ses propres adresses restent intactes.
  5. En retrait mode = "pickup"), aucune adresse n’est requise : seul pickup_location_id compte. Indiquez le destinataire tiers via metadata.note.

Réponse

200 + OrderDetailResource, avec un champ additionnel payment_link :

{

  "data": {

    "id": "<order_ulid>",

    "reference": "CMD-000123",

    "amount": 32000,

    "amount_due": 32000,

    "currency": "XAF",

    "billing": { "method": "cash", "status": "pending", "total": 32000, "net_to_pay": 32000, "address": { } },

    "shipping": { "mode": "delivery", "status": "pending", "amount": 2000, "address": { } },

    "products": [ ],

    "discounts": [ ],

    "deliveries": [ ],

    "payments": [ ]

  },

  "payment_link": "[https://checkout.stripe.com/](https://checkout.stripe.com/)..."

}
payment_link* est renseigné automatiquement quand deferred_payment est absent ou
false (paiement immédiat). Si deferred_payment = true, il vaut null et vous générez
le lien plus tard (§4.9).

Connecter le client au checkout authenticate)

Par défaut, POST /customers/orders ne renvoie pas de token : seuls register et login en émettent. Pour authentifier automatiquement l’acheteur juste après le checkout (y compris en checkout invité, où le client n’a pas de mot de passe), ajoutez "authenticate": true au payload. La réponse contient alors un token OAuth et la ressource customer, en plus de data :

{

  "data": { "id": "<order_ulid>", "reference": "CMD-000123" },

  "payment_link": null,

  "token": "<oauth_access_token>",

  "customer": { "id": "<customer_ulid>", "first_name": "Jean", "email": "[[email protected]](mailto:[email protected])" }

}
Placez ensuite ce token en Authorization: Bearer <token> pour accéder au profil, aux adresses et à l’historique de commandes du client — sans appel registerlogin séparé.
Le token est émis pour le client de la commande (celui résolu/créé par le pipeline). En
checkout connecté (requête déjà porteuse d’un Bearer), authenticate renvoie simplement un
nouveau token pour ce même client.

Autres routes commande

| Méthode | Endpoint | Auth | Notes | |---|---|---|---| | GET | /2023-11/customers/orders/{orderId} | publique X-Company) | Détail d’une commande. | | GET | /2023-11/customers/orders | Bearer | Commandes du client connecté (paginé). | | PUT | /2023-11/customers/orders/{order} | Bearer | Mise à jour (même pipeline). |

4.9 Paiement

Deux modes : A. Paiement immédiatdeferred_payment absentfalse à la création. Le payment_link est renvoyé directement dans la réponse de POST /customers/orders. Redirigez le client dessus. B. Paiement différédeferred_payment = true, puis :

POST /2023-11/customers/orders/{order}/payments

{ "amount_due": 32000, "success_url": "https://...", "cancel_url": "https://..." }
  • amount_due optionnel (défaut : order.amount_due).
  • success_url / cancel_url (alias : callback).
  • Réponse : 200 { "url": "https://..." }, ou 400 { "message": "Payment link could not be generated" }.
Providers supportés (sélectionnés selon billing.method = processor du moyen, §4.1) : Stripe, Paydunya, Pawapay, PayPal, Mamoni, Taramoney, Flutterwave, NotchPay (+ cash hors ligne). Les webhooks de chaque provider mettent à jour le statut de paiement de la commande de façon asynchrone.
Endpoints provider directs POST /2023-11/{stripe|paydunya|pawapay|mamoni|paypal|taramoney}/checkout)
existent mais sont des détails d’implémentation : passez par payment_link / /payments.

4.10 Suivi de livraison

OrderDetailResource inclut delivery (la première) et deliveries[]. | Méthode | Endpoint | Notes | |---|---|---| | GET | /2023-11/delivery/{delivery} | Détail livraison. | | GET | /2023-11/deliveries | Liste paginée (filtres tracking_number, order_id, status, mode, scheduled_at, search…). | | GET | /2023-11/delivery/{delivery}/last_position | Dernière position connue. | DeliveryResource* — champs principaux :

{

  "id": "<ulid>", "tracking_number": "TRK-00123", "tracking_url": "[https://…](https://…)",

  "order_id": "<ulid>", "status": "taken_in_charge", "mode": "default",

  "scheduled_at": "…", "starts_at": null, "ends_at": null,

  "address": { },

  "products": [ { "title": "…", "quantity": 2 } ],

  "delivery_status": { "is_complete": false, "is_partial": true, "delivered_quantity": 1, "sequence": 1, "is_first": true },

  "user": { "totalDeliveries": 12 }

}
statusidle, pending, taken_in_charge, partial_delivery, delivered, cancelled
(enum App\Enum\DeliveryStatus). Une commande peut être livrée en plusieurs fois :
deliveries[] porte chaque tournée, et delivery_status indique l’avancement (quantité livrée,
séquence, livraison partielle).
Position — réponse de /last_position (snapshot DeliveryTracking, pas un flux temps réel) :

{

  "delivery": { "id": "<ulid>", "status": "taken_in_charge", "order_id": "<ulid>" },

  "position": { "latitude": 4.0511, "longitude": 9.7679, "accuracy": 10, "heading": 120, "speed": 5.5, "updated_at": "…" }

}
Si aucune position n’a encore été enregistrée : { "message": "Aucune position disponible" }.
Pour un suivi « live », faites du polling sur cet endpoint. Affichez tracking_number,
tracking_url et la position (lat/lng + heading pour orienter une icône sur la carte).

4.11 Documents de commande (médias)

La tâche ManageMediasTask lit le tableau medias[] (ou son alias images[]) à la création et à la mise à jour. Formats acceptés (clés exactes) :

{

  "medias": [

    { "src": "[https://exemple.com/facture.pdf](https://exemple.com/facture.pdf)" },

    { "base64": "data:application/pdf;base64,JVBER...", "name": "bon-commande.pdf" },

    { "file": "<fichier uploadé>" },

    { "id": "<media_id_existant>" }

  ]

}
  • { "id": "..." } lors d’un PUT conserve un média déjà attaché (les médias absents du tableau sont supprimés).
  • Lecture : OrderDetailResource expose medias (publics) et private_medias (signature/privés).

4.12 Collections (catalogue groupé)

Les collections regroupent des produits (univers, catégories, sélections) et peuvent être imbriquées parent_collection_id + sub_collections). Elles servent à bâtir la navigation et les pages de catégorie. Le filtre filter[collections]=id1,id2 de /products (§4.2) s’appuie dessus. | Méthode | Endpoint | Notes | |---|---|---| | GET | /2023-11/collections | Liste paginée. | | GET | /2023-11/collections/count | Nombre de collections. | | GET | /2023-11/collections/{collection} | Détail CollectionMinimalModel). | | GET | /2023-11/collections/handle/{handle} | Détail par slug. | | GET | /2023-11/collections/{collection}/products | Produits de la collection ProductDetailResource). | | GET | /2023-11/collections/handle/{handle}/products | Idem par slug. | Filtres filter[…]) : title, handle, content (partiels), search, withProducts (collections non vides uniquement), updated_at (plage { start, end }). Includes : tags, productsCount, subCollections, products. Tris : title, handle, products_count, sub_collections_count, created_at, updated_at. Champs CollectionMinimalModel) : id, title, handle, content, parent_collection_id, metadata, medias[], et products[] (uniquement si ?include=products).
/collections/{id}/products renvoie des ProductDetailResource complets (prix, variantes,
stock, médias) — c’est l’endpoint à câbler sur une page catégorie. Pour ne lister que les
collections ayant du stock à afficher, ajoutez filter[withProducts]=1.

4.13 Contenu éditorial — Blogs, Articles, Pages (CMS)

Genuka expose un CMS léger pour le storefront : un blog contient des articles ; les pages sont des contenus statiques (À propos, CGV, FAQ…). CompanyResource.hasBlogs (§4.1) indique si un blog existe. | Méthode | Endpoint | Notes | |---|---|---| | GET | /2023-11/blogs | Liste des blogs filter[search], include=articles, sort=title). | | GET | /2023-11/blogs/{blog} · /blogs/handle/{handle} | Détail blog. | | GET | /2023-11/articles | Liste d’articles (paginée). | | GET | /2023-11/articles/count | Nombre d’articles. | | GET | /2023-11/articles/{article} · /articles/handle/{handle} | Détail article. | | GET | /2023-11/pages | Liste des pages CMS. | | GET | /2023-11/pages/count | Nombre de pages. | | GET | /2023-11/pages/{page} · /pages/handle/{handle} | Détail page. | Articles — filtres : published, blog_id, blog_handle, created_at (plage), search (titre, handle, auteur, description, contenu, blog, tags). Includes : tags, blog. Tris : title, handle, published, vendor, created_at, updated_at (défaut title). Champs ArticleResource) : id, title, handle, description, content, author, published, blog_id, blog (objet), metadata, medias[], created_at, updated_at. Blogs — filtre search (titre/handle + articles). Include articles. Champs BlogResource) : id, title, handle, metadata, medias[], created_at, updated_at. Pages — filtre search (titre/handle). Champs PageResource) : id, title, handle, content, metadata, medias[], created_at, updated_at.
SEO / rendu : utilisez toujours les routes handle/{slug} pour des URLs propres
/blog/mon-article, /p/a-propos). content est du HTML/markdown éditorial à rendre tel quel ;
medias[] fournit l’image de couverture (champs microthumblargewebp).

4.14 Réservation de services — disponibilités & créneaux

Pour les entreprises de services (prise de rendez-vous), un produit de type service est lié à des professionnels User) qui ont des disponibilités récurrentes et des indisponibilités (jours bloqués). Le front affiche un calendrier puis des créneaux réservables. | Méthode | Endpoint | Notes | |---|---|---| | GET | /2023-11/availabilities?user_id=<id> | Horaires récurrents du professionnel. | | GET | /2023-11/availabilities/slots?user_id=<id>&service_id=<id>&day=<date> | Créneaux libres calculés pour une date. | | GET | /2023-11/availabilities/{availability} | Détail d’une plage récurrente. | | GET | /2023-11/unavailabilities?user_id=<id> | Jours d’indisponibilité (filtres start_date, end_date). | | GET | /2023-11/unavailabilities/dates?user_id=<id> | Tableau de dates ["2026-05-23", …] pour griser un calendrier. | | GET | /2023-11/unavailabilities/{unavailability} | Détail. | availabilities* — champs : user_id, day_of_week (0=dimanche → 6), start_time, end_time H:i:s), timezone, metadata. availabilities/slots* — paramètres requis service_id et day (date), user_id optionnel (défaut = utilisateur authentifié). Réponse calculée côté serveur :

{

  "status": true,

  "data": {

    "professional_id": "<ulid>",

    "professional_name": "…",

    "unavailable": false,

    "slots": [

      { "start": "09:00", "end": "09:30", "duration": 30, "buffer": 10, "available": true }

    ]

  }

}
Le moteur part des plages availabilities du jour, découpe en créneaux de
service.duration_minutes espacés de + buffer_minutes, exclut les rendez-vous déjà pris
CalendarEvent), les créneaux passés et ceux qui débordent. Si le jour est marqué indisponible,
unavailable = true et slots = [].
Parcours de réservation conseillé :
  1. unavailabilities/dates → griser les jours bloqués du calendrier.
  2. À la sélection d’un jour : availabilities/slots → afficher les créneaux available: true.
  3. Le client choisit un créneau, puis vous créez la commande (§4.8) en plaçant l’heure du RDV dans shipping.scheduled_at (et/ou metadata).

4.15 Chat storefront (messagerie temps réel)

Genuka fournit un webchat embarquable : le visiteur discute avec l’entreprise depuis la boutique. Ces routes ne nécessitent pas X-Company (l’entreprise est déduite du **jeton de conversation**), mais un middleware validate.chat.token et un throttle:chat. | Méthode | Endpoint | Notes | |---|---|---| | GET | /2023-11/inbox/chat/{conversation}/messages | Messages (cursor-paginés, per_page défaut 50). Les messages internes ne sont jamais renvoyés. | | POST | /2023-11/inbox/chat/{conversation}/messages | Envoyer un message. | | POST | /2023-11/inbox/chat/{conversation}/typing | Indicateur « en train d’écrire ». | | POST | /2023-11/inbox/chat/{conversation}/read | Marquer comme lu. | | POST | /2023-11/inbox/chat/{conversation}/request-unlock | Demande de déblocage (OTP) — sans token. | Envoi d’un message :

{ "type": "text", "content": { "body": "Bonjour, ce produit est-il dispo ?" } }
  • type requis ; content.body requis si type=text (max 5000 caractères). Les types internes/système sont refusés 403).
  • Réponse : la ressource MessageResource créée.
  • La liste de messages renvoie aussi un return_link vers le canal d’origine (WhatsApp, Messenger, Instagram, Telegram) quand la conversation y est rattachée.
Ce module est optionnel pour une boutique : utile pour le support / la conversion. Le jeton de
conversation est émis par le widget de chat lors de l’ouverture d’une session.

4.16 Factures & documents publics

| Méthode | Endpoint | Notes | |---|---|---| | GET | /2023-11/invoices/{orderId} | Affiche la facture liée à une commande. | | GET | /2023-11/invoices/{orderId}/invoice | Version imprimable (PDF/HTML). | Pratique pour proposer un lien « Télécharger ma facture » sur la page de confirmation / suivi de commande, sans authentification (lorderId fait office de jeton d’accès — ne l’exposez pas publiquement au-delà du client concerné).

4.17 Plans (hors boutique)

| Méthode | Endpoint | Notes | |---|---|---| | GET | /2023-11/plans | Liste des plans d’abonnement Genuka. | | GET | /2023-11/plans/{lookup_key} | Détail d’un plan. | Ces routes concernent l’abonnement de l’entreprise à Genuka, pas le storefront. À ignorer pour un site e-commerce classique ; documentées ici par exhaustivité (routes publiques).

5. Multi-devise

  • CompanyResource expose currency (devise par défaut) et currencies[] (additionnelles).
  • Chaque Shop peut porter sa propre currency_code / currency_name.
  • Devise d’une commande résolue : currency_code (requête) → boutique → entreprise → XAF.
Recommandations front :
  1. Afficher la devise active (boutique avant entreprise).
  2. Figer les prix transactionnels dans la devise de la commande.
  3. Ne jamais reconvertir a posteriori une commande déjà créée.

6. Multi-boutique

  • Liste des boutiques publiques via /shops.
  • Scope via X-ShopId sur catalogue, stock, prix, checkout, suivi.
  • Relations boutiques ↔ entrepôts pour le stock.
Recommandations front :
  1. Imposer le choix d’une boutique au début du parcours.
  2. Propager X-ShopId partout.
  3. Isoler le panier par boutique (devise, stock et prix diffèrent).

7. Conventions & gestion des erreurs

| Code | Signification | Forme | |---|---|---| | 200 | Succès | data ou objet métier | | 401 | Non authentifié (token manquant/invalide) | { "message": "..." } | | 404 | Ressource introuvable | { "message": "..." } | | 422 | Validation / X-Company manquant | { "message": "...", "errors": { } } | | 400 | Erreur métier (stock, doublon client, lien de paiement…) | { "message": "..." } |
  • Pagination : enveloppe Laravel { data, links, meta }. Contrôle via per_page ou page[size].
  • Filtres/tris/includes : convention Spatie QueryBuilder filter[x], sort, include).
  • Idempotence : l’API détecte les doublons CheckDuplicateOrderTask), mais protégez aussi le bouton de paiement côté front (désactivation + clé d’idempotence applicative).

8. Checklist e-commerce (à ne pas oublier)

  1. Panier persistant (invité + connecté) et fusion à la connexion — géré côté front.
  2. Idempotence du POST /customers/orders (anti double-commande).
  3. Revérification serveur des prix/stock : ne jamais faire confiance au front (déjà appliqué par le pipeline).
  4. Taxes complètes (produits, livraison, remises) — calculées côté serveur, à afficher fidèlement.
  5. E-mails transactionnels : confirmation, paiement, expédition, échec.
  6. Webhooks / polling pour synchroniser statut commande/paiement/livraison.
  7. Remboursements, retours, notes de crédit returns, credit_notes dans OrderDetailResource).
  8. SEO catalogue (slug handle, métadonnées, schema.org).
  9. Conformité RGPD/PII (rétention, droit à l’effacement).
  10. Observabilité (correlation id dans les logs, alertes paiement/webhook).
  11. Anti-abus du checkout (rate limit, captcha selon le risque).
  12. Tests E2E du parcours complet (catalogue → panier → paiement → suivi).

9. Roadmap d’implémentation conseillée

| Phase | Contenu | |---|---| | 1 | Contexte entreprise + boutique · liste/détail produits + variantes · panier local | | 2 | Auth client + adresses · checkout (livraison/retrait + remise) · création de commande | | 3 | Lien de paiement + callbacks providers · pages suivi commande/livraison · documents | | 4 | Multi-devise · multi-boutique · durcissement (idempotence, sécurité, monitoring) |
⚠️ La sécurité, l’idempotence et le monitoring (phase 4) ne sont pas optionnels : intégrez-les
au plus tôt plutôt que de les repousser en fin de projet.