{"openapi":"3.0.1","info":{"title":"DZ Relay — API publique partenaires","description":"API REST pour intégrer DZ Relay dans votre ERP, CMS ou plug-in e-commerce.\n\n## Démarrage rapide\n\n1. **Créez une clé** dans le back-office DZ Relay sous *Paramètres → Clés API*.\n2. **Stockez-la** dans votre application — elle ne sera plus affichée.\n3. **Authentifiez** chaque requête avec l'en-tête `Authorization: ApiKey dzlk_…`.\n4. **Testez** : `GET /api/partner/v1/health` renvoie votre identifiant d'organisation.\n\n## Rate limit\n\n60 requêtes par minute par clé (paramétrable par l'admin de DZ Relay).\nHeaders de réponse : `X-RateLimit-Limit`, `X-RateLimit-Remaining`.\nEn cas de dépassement : `429 Too Many Requests` + `Retry-After` en secondes.\n\n## Webhooks sortants\n\nConfigurez votre URL et secret HMAC dans *Paramètres → Webhooks* du back-office.\nChaque transition d'état d'un de vos colis déclenche un `POST signé HMAC-SHA256`\nsur votre endpoint avec les en-têtes :\n- `X-Dzrelay-Event` (ex `parcel.delivered`)\n- `X-Dzrelay-Delivery-Id` (UUID — pour l'idempotence)\n- `X-Dzrelay-Signature: sha256=<hex>` (HMAC du body avec votre secret)\n\n## Codes d'erreur\n\nToutes les erreurs métier sont normalisées :\n```json\n{ \"code\": \"RELAY_NOT_FOUND\", \"message\": \"...\", \"status\": 400 }\n```\n","contact":{"name":"DZ Relay — Support intégrateurs","email":"dev@dzrelay.com"},"license":{"name":"Conditions générales d'utilisation API","url":"https://api.dzrelay.com/cgu-api"},"version":"v1"},"servers":[{"url":"https://api.dzrelay.com","description":"Production"}],"security":[{"ApiKeyAuth":[]}],"tags":[{"name":"Sandbox","description":"Outils de test pour intégrateurs (clés dzsk_… uniquement)."},{"name":"Colis","description":"Création, lecture et annulation de colis."},{"name":"Sentinel","description":"Endpoint de découverte / health check."}],"paths":{"/api/partner/v1/sandbox/parcels/{id}/trigger-event":{"post":{"tags":["Sandbox"],"summary":"Simule une transition de statut (sandbox uniquement)","description":"Émet un événement de changement de statut sur un colis sandbox qui appartient à votre organisation. Le webhook correspondant est envoyé à votre URL configurée avec le payload standard (sandbox=true).","operationId":"triggerEvent","parameters":[{"name":"id","in":"path","description":"Identifiant interne du colis sandbox.","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TriggerRequest"}}},"required":true},"responses":{"404":{"description":"Colis introuvable.","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TriggerResponse"}}}},"400":{"description":"Statut cible invalide.","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TriggerResponse"}}}},"403":{"description":"Clé non-sandbox, ou colis d'une autre organisation, ou colis non-sandbox.","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TriggerResponse"}}}},"202":{"description":"Événement programmé.","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TriggerResponse"}}}}}}},"/api/partner/v1/parcels":{"get":{"tags":["Colis"],"summary":"Liste paginée des colis du marchand","description":"Renvoie uniquement les colis appartenant à l'organisation rattachée à la clé API (filtrage automatique, pas d'usurpation possible). Tri par date de création décroissante.","operationId":"list","parameters":[{"name":"status","in":"query","description":"Filtrer par statut (CREATED, READY_FOR_PICKUP, PICKED_UP, IN_TRANSIT, AT_RELAY, DELIVERED, RETURN_PENDING, RETURNED, LOST, DAMAGED, CANCELLED)","required":false,"schema":{"type":"string","enum":["CREATED","READY_FOR_PICKUP","PICKED_UP","IN_TRANSIT","AT_RELAY","RELAY_NOTIFIED","DELIVERED","RETURN_PENDING","RETURNED","LOST","DAMAGED","CANCELLED"]}},{"name":"page","in":"query","description":"Numéro de page (zéro-indexé)","required":false,"schema":{"minimum":0,"type":"integer","format":"int32","default":0}},{"name":"size","in":"query","description":"Taille de page (1-100, défaut 20)","required":false,"schema":{"maximum":100,"minimum":1,"type":"integer","format":"int32","default":20}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ParcelPage"}}}}}},"post":{"tags":["Colis"],"summary":"Créer un colis","description":"Le débit du wallet marchand a lieu immédiatement ; en cas de solde insuffisant, l'API renvoie `402 Payment Required`. Le tracking number généré est retourné dans la réponse et peut être utilisé pour le suivi public.","operationId":"create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateParcelRequest"}}},"required":true},"responses":{"429":{"description":"Rate-limit dépassé.","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ParcelView"}}}},"403":{"description":"Scope `parcels:write` requis.","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ParcelView"}}}},"400":{"description":"Validation : tarif ou code relais invalide.","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ParcelView"}}}},"201":{"description":"Colis créé. L'en-tête `Location` pointe vers le détail.","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ParcelView"}}}},"402":{"description":"Solde wallet insuffisant pour ce colis.","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ParcelView"}}}}}}},"/api/partner/v1/parcels/{id}/cancel":{"post":{"tags":["Colis"],"summary":"Annuler un colis avant collecte","description":"Transition autorisée uniquement depuis `CREATED` ou `READY_FOR_PICKUP`. Au-delà (collecte effectuée), l'annulation passe par le support DZ Relay car elle implique une coordination logistique (et un éventuel remboursement).","operationId":"cancel","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelRequest"}}}},"responses":{"200":{"description":"Colis annulé, statut = CANCELLED.","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ParcelView"}}}},"409":{"description":"État non annulable (colis déjà en transit ou livré).","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ParcelView"}}}},"404":{"description":"Colis introuvable ou non rattaché à votre organisation.","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ParcelView"}}}}}}},"/api/partner/v1/parcels/{id}":{"get":{"tags":["Colis"],"summary":"Détail d'un colis","description":"404 si le colis n'existe pas OU appartient à un autre marchand (pas de divulgation d'existence cross-organisation).","operationId":"get","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ParcelView"}}}}}}},"/api/partner/v1/parcels/by-tracking/{trackingNumber}":{"get":{"tags":["Colis"],"summary":"Lookup d'un colis par tracking number","description":"Utile pour retrouver un colis sans avoir mémorisé son UUID interne. 404 si introuvable ou non rattaché à votre organisation.","operationId":"byTracking","parameters":[{"name":"trackingNumber","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ParcelView"}}}}}}},"/api/partner/v1/health":{"get":{"tags":["Sentinel"],"summary":"Ping authentifié","description":"Renvoie 200 si votre clé API est valide. Utilisez ce endpoint pour vérifier votre intégration sans risquer d'effet de bord métier. L'en-tête `X-RateLimit-Remaining` indique votre quota restant pour la minute en cours.","operationId":"health","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PartnerHealthResponse"}}}}}}}},"components":{"schemas":{"TriggerRequest":{"required":["toStatus"],"type":"object","properties":{"toStatus":{"type":"string","description":"Statut cible à appliquer.","example":"DELIVERED","enum":["CREATED","READY_FOR_PICKUP","PICKED_UP","IN_TRANSIT","AT_RELAY","RELAY_NOTIFIED","DELIVERED","RETURN_PENDING","RETURNED","LOST","DAMAGED","CANCELLED"]},"reason":{"type":"string","description":"Motif libre (apparaîtra dans le payload webhook)."}},"description":"Demande de transition manuelle d'un colis sandbox."},"TriggerResponse":{"type":"object","properties":{"parcelId":{"type":"string","format":"uuid"},"trackingNumber":{"type":"string"},"previousStatus":{"type":"string"},"newStatus":{"type":"string"},"triggeredAt":{"type":"string","format":"date-time"}},"description":"Confirmation de transition sandbox."},"CreateParcelRequest":{"required":["destinationRelayCode","recipientName","recipientPhone","weightG"],"type":"object","properties":{"recipientName":{"maxLength":100,"minLength":0,"type":"string","description":"Nom du destinataire (ne sera pas affiché publiquement, stocké chiffré).","example":"Hocine Benali"},"recipientPhone":{"pattern":"^\\+?[0-9 ]{8,20}$","type":"string","description":"Téléphone DZ du destinataire (format permissif). Le SMS de retrait sera envoyé à ce numéro.","example":"0555 12 34 56"},"weightG":{"maximum":30000,"minimum":1,"type":"integer","description":"Poids en grammes. Tarif calculé par paliers (cf. /tarifs sur le site).","format":"int32","example":1500},"lengthCm":{"type":"integer","description":"Longueur en cm (optionnel).","format":"int32"},"widthCm":{"type":"integer","description":"Largeur en cm (optionnel).","format":"int32"},"heightCm":{"type":"integer","description":"Hauteur en cm (optionnel).","format":"int32"},"destinationRelayCode":{"pattern":"^DZ-[A-Z0-9-]{3,30}$","type":"string","description":"Code public du point relais de destination (visible sur la carte des relais).","example":"DZ-ALG-001"},"declaredValueDzd":{"type":"number","description":"Valeur déclarée en DZD pour assurance (optionnel).","example":5000},"codAmountDzd":{"type":"number","description":"Montant à encaisser en espèces à la remise (Cash on Delivery). Omettre ou null = pas de COD. Indépendant de la valeur déclarée.","example":8500}},"description":"Payload de création d'un colis."},"ParcelView":{"type":"object","properties":{"id":{"type":"string","description":"Identifiant interne DZ Relay.","format":"uuid"},"trackingNumber":{"type":"string","description":"Numéro public utilisable pour le suivi.","example":"DZ34858428237"},"status":{"type":"string","description":"État courant.","example":"CREATED"},"destinationRelayCode":{"type":"string","description":"Code du point relais de destination.","example":"DZ-ALG-001"},"labelUrl":{"type":"string","description":"URL pré-signée pour télécharger l'étiquette PDF (validité ~30 min). Peut être null si MinIO momentanément KO — re-tenter via GET."},"pricePaidDzd":{"type":"number","description":"Prix débité sur votre wallet en DZD.","example":350},"codAmountDzd":{"type":"number","description":"Montant Cash on Delivery à encaisser à la remise (null si pas de COD).","example":8500},"createdAt":{"type":"string","description":"Date de création UTC.","format":"date-time"},"updatedAt":{"type":"string","description":"Date de dernière modification UTC.","format":"date-time"}},"description":"Vue partenaire d'un colis."},"CancelRequest":{"type":"object","properties":{"reason":{"type":"string"}}},"ParcelPage":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ParcelView"}},"page":{"type":"integer","format":"int32"},"size":{"type":"integer","format":"int32"},"totalElements":{"type":"integer","format":"int64"},"totalPages":{"type":"integer","format":"int32"}}},"PartnerHealthResponse":{"type":"object","properties":{"status":{"type":"string","description":"Toujours \"ok\" si la clé est valide.","example":"ok"},"organizationId":{"type":"string","description":"Identifiant interne de votre organisation marchande DZ Relay.","format":"uuid"},"organizationName":{"type":"string","description":"Raison sociale de votre organisation.","example":"Boutique Démo Alger"},"keyPrefix":{"type":"string","description":"Préfixe lisible de la clé utilisée (sans le secret).","example":"dzlk_a1b2c3"},"serverTime":{"type":"string","description":"Horloge serveur (UTC) pour aider à diagnostiquer un éventuel skew.","format":"date-time"}},"description":"Réponse du health check authentifié."}},"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","description":"Format attendu : `ApiKey <votre-clé-dzlk_...>`. Créez votre clé dans le back-office sous Paramètres → Clés API. Alternative : en-tête `X-API-Key`.","name":"Authorization","in":"header"}}}}