V1.0.0 Stable

Documentation API Arel Pro

Créez des envois, consultez les tarifs et recevez les événements de suivi. Parcourez section par section.

Webhooks

Webhooks

Arel appelle votre URL en POST sur cinq événements. La couverture dépend du chemin emprunté : seul shipping.created est systématique.

S'abonner

bash
curl -X POST "https://api.arelpro.com/pro/api/v1/webhooks" \
  -H "Authorization: Bearer sk_live_VOTRE_CLE" \
  -H "X-Client-Id: VOTRE_CLIENT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://mon-site.fr/webhooks/arel",
    "events": ["shipping.assigned", "shipping.delivered"]
  }'

# Le secret est genere automatiquement et renvoye dans la reponse.
# Il reste lisible ensuite via GET /pro/api/v1/webhooks
EndpointScopeNotes
GET /webhooksreadListe — renvoie le secret en clair
POST /webhookswrite{ url, events, secret?, isActive? } → 201
PUT /webhooks/:idwriteurl, events, isActive
DELETE /webhooks/:iddeleteScope delete — absent du défaut

Le secret est généré automatiquement si vous n'en fournissez pas. Il est renvoyé en clair par GET /webhooks ainsi qu'à la création : contrairement aux clés API, il reste récupérable a posteriori. Traitez-le comme un mot de passe. Un seul webhook par couple (compte, URL).

Événements

shipping.createdshipping.assignedshipping.picked_upshipping.deliveredshipping.cancelled

Ces cinq valeurs sont les seules acceptées par POST /webhooks. Il n'y a pas d'événement shipping.in_transit : in_transit est un statut, jamais un événement. Le passage en in_transit ne déclenche aucun webhook.

Avant de bâtir une réconciliation sur ces événements

Un webhook n'est pas émis par un changement de statut : il est émis par le code qui effectue ce changement. Or tous les chemins qui font avancer un bordereau n'émettent pas. shipping.picked_up et shipping.delivered ne partent que si le livreur passe par le chemin qui émet.

Ne considérez jamais l'absence de shipping.delivered comme la preuve qu'un colis n'est pas livré. Traitez ces événements comme une accélération — pas comme une source de vérité. L'état fiable est celui que renvoie GET /shipping/:id ; prévoyez une réconciliation périodique par sondage.

ÉvénementPart quand…Ne part pas quand…
shipping.createdToujours, une fois par bordereau, quelle que soit l'origine (API, /batch, dashboard, Shopify…). Point d'émission unique.
shipping.assignedLe dispatch automatique attribue la course ; un livreur l'accepte depuis l'app ; un administrateur (ré)assigne. Bonne couverture.Assignation via le canal temps réel de l'app livreur.
shipping.picked_upLe livreur confirme l'enlèvement par le chemin HTTP de l'app.Passage par le canal temps réel → aucun webhook.
shipping.deliveredLe livreur confirme la livraison par le chemin HTTP de l'app.Passage par le canal temps réel → aucun webhook.
shipping.cancelledVous annulez — par PUT /shipping/:id/cancel ou depuis le dashboard.Un livreur abandonne une course : le bordereau repart en created pour retrouver un livreur — ce n'est pas une annulation et rien n'est émis.

shipping.created est le seul événement à la couverture totale. Il part exactement une fois par bordereau, quelle que soit l'origine — API unitaire, /batch, dashboard, import, Shopify, création programmée.

data.status y vaut created ou programmed. Un bordereau programmé émet l'événement une seule fois, à sa création, avec status: "programmed" ; son passage ultérieur à created ne ré-émet pas. shipping.created signifie « cette ressource existe », pas « ce statut vient de changer ».

L'ordre d'arrivée n'est pas garanti. Les émissions sont asynchrones et non séquencées : shipping.assigned peut arriver avant shipping.created. Ne construisez pas de machine à états sur l'ordre de réception — utilisez le champ timestamp de l'enveloppe.

json
{
  "event": "shipping.assigned",
  "timestamp": "2026-07-16T10:00:00.000Z",
  "data": {
    "id": "665f...",
    "professionalId": "664a...",
    "referenceNumber": "AREL-20260716-00042",
    "status": "assigned",
    "price": 9,
    "externalReference": { "platform": "api", "orderId": "CMD-1001" },
    "createdAt": "2026-07-16T10:00:00.000Z",
    "updatedAt": "2026-07-16T10:02:00.000Z",
    "pickupAddress": { },
    "deliveryAddress": {
      "recipientName": "Jean Dupont",
      "street": "123 Rue de la République",
      "city": "Paris",
      "postalCode": "75001",
      "country": "France",
      "phoneNumber": "+33612345678"
    },
    "deliverer": {
      "id": "6612a9f4c3b25d1e8a77f430",
      "name": null,
      "phoneNumber": null
    }
  }
}

data est une liste blanche : les champs ci-dessus, et eux seuls. Ni deliveryCode, ni metadata, ni notes, ni pdfUrl.

externalReference est recopié intégralement — c'est votre pont vers votre commande : vous y retrouvez le orderId que vous aviez passé à la création, sans avoir à relire le bordereau. Attention au revers : tout ce que vous mettez dans externalReference ressortira dans la charge utile envoyée à votre URL. N'y mettez rien de sensible ; pour des données que vous ne voulez pas voir repartir, utilisez metadata — jamais inclus dans les webhooks (il reste lisible sur GET /shipping/:id).

deliverer.name et deliverer.phoneNumber valent toujours null

Aucun des points d'émission ne résout l'identité du livreur avant d'envoyer. Vous recevez donc null si aucun livreur n'est assigné, et sinon { id: "<identifiant>", name: null, phoneNumber: null }.

Seul deliverer.id porte de l'information. Pour le nom ou le téléphone, appelez GET /shipping/:id à réception de l'événement — n'affichez jamais data.deliverer.name à un utilisateur, il sera vide.

Vérifier la signature

Le schéma est de type Stripe (horodatage préfixé), malgré le préfixe sha256= d'allure GitHub. Une recette GitHub signe le corps seul — elle échouera.

signature = HMAC_SHA256(secret, X-Arel-Timestamp + "." + corps_brut)

En-têteContenu
X-Arel-EventType d'événement
X-Arel-TimestampHorodatage ISO de la signature
X-Arel-Signaturesha256=<hmac hex>

Deux points d'attention pour la vérification

1. Le timestamp du corps n'est pas celui à signer. Le JSON contient un champ timestamp, distinct de l'en-tête X-Arel-Timestamp. Signez l'en-tête. À la première tentative, les deux sont produits dans la même milliseconde et sont donc souvent identiques. Aux réessais, l'en-tête est recalculé et les deux divergent — votre vérification se met à rejeter. Cette erreur ne se voit pas en test : elle se révèle en production, sous charge.

2. La signature change à chaque tentative. X-Arel-Timestamp est recalculé à chaque réessai : deux tentatives du même événement portent le même corps mais des signatures différentes. Ne dédupliquez jamais sur la signature — dédupliquez sur data.id + event.

Signez le corps brut, tel qu'octets reçus, avant tout parsing : ré-encoder l'objet parsé peut modifier le JSON et invalider la signature.

javascript
const express = require('express');
const crypto = require('crypto');

const WEBHOOK_SECRET = 'votre_secret_webhook';
const app = express();

function verifyArelSignature(rawBody, timestamp, signature) {
  if (!timestamp || !signature) return false;

  // Le corps signe est : X-Arel-Timestamp + "." + corps brut
  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  const received = signature.startsWith('sha256=') ? signature.slice(7) : signature;
  const a = Buffer.from(expected);
  const b = Buffer.from(received);

  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post(
  '/webhooks/arel',
  express.raw({ type: 'application/json' }), // corps brut obligatoire
  (req, res) => {
    const ok = verifyArelSignature(
      req.body.toString('utf8'),
      req.get('X-Arel-Timestamp'),
      req.get('X-Arel-Signature')
    );

    if (!ok) return res.sendStatus(401);

    // Repondez AVANT de traiter : le timeout est de 5 s, et un traitement
    // lent provoquerait un reessai, donc un doublon. L'accuse de reception
    // dit « signature valide, evenement recu », pas « traitement termine ».
    res.sendStatus(200);

    // Tout ce qui suit se fait apres la reponse, hors du chemin critique.
    let event;
    try {
      event = JSON.parse(req.body.toString('utf8'));
    } catch (err) {
      console.error('Corps illisible :', err);
      return;
    }

    // Dedupliquez sur data.id + event, JAMAIS sur la signature.
    if (event.event === 'shipping.created') {
      console.log('Bordereau cree :', event.data.referenceNumber);
    }
  }
);

Réessais

  • 3 tentatives maximum.
  • Délais avant tentative : 0 ms, 1 000 ms, 2 000 ms. Fixes, sans jitter.
  • Timeout : 5 000 ms par tentative.
  • Est un échec tout statut HTTP non-2xx, ainsi que le timeout.
  • Après 3 échecs, Arel abandonne : l'événement est perdu. Il n'y a ni file de rejeu, ni rattrapage.

Tout se joue en ~3 secondes : votre endpoint doit répondre 2xx immédiatement et traiter en asynchrone. Un traitement lourd effectué avant la réponse provoquera le timeout, donc un doublon. Compte tenu des réessais, votre endpoint doit être idempotent — dédupliquez sur data.id + event.

Vous êtes un particulier ?

L'application Arel envoie vos biens.

Un bien à faire parvenir à quelqu'un, livré le jour même par la communauté. Suivi et remise sécurisée dans l'application.

Découvrir l'application