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.

Gestion d'erreurs

Codes d'erreur

Branchez sur le statut HTTP et sur le slug. Jamais sur le message.

Enveloppe

Toute erreur du contrôle d'accès (clé, Client ID, scope) renvoie exactement ces trois champs. Les refus métier y ajoutent des champs de contexte, sans jamais en retirer.

json
{
  "success": false,
  "error": "INVALID_API_KEY",
  "message": "Clé API invalide ou révoquée"
}
  • Branchez sur le statut HTTP et sur error. C'est un slug stable, en majuscules, destiné au code.
  • Ne branchez jamais sur message. C'est de la prose française destinée à un humain : elle peut être reformulée ou traduite à tout moment, sans préavis. Elle n'est pas contractuelle.
  • Il n'y a aucun champ code numérique. Si une ancienne documentation vous a fait écrire if (err.code === 4001), ce code n'a jamais existé : supprimez-le.
  • Trois exceptions à cette enveloppe : les erreurs de champ (tableau errors, aucun slug) ; POST /shipping/batch, qui inverse error et code ; et les 400 sans slug de POST /rates et POST /shipping.

Slugs d'authentification

HTTPerrorCause
401MISSING_API_KEYEn-tête Authorization absent, mal formé, ou clé vide
401MISSING_CLIENT_IDAucun clientId (en-tête, query ou body)
401INVALID_API_KEYClé inconnue ou révoquée
401EXPIRED_API_KEYDate d'expiration dépassée
401PROFESSIONAL_NOT_FOUNDCompte lié à la clé introuvable
403INVALID_CLIENT_IDLe Client ID ne correspond pas au propriétaire de la clé
403INSUFFICIENT_PERMISSIONSScope manquant pour cette méthode HTTP
500INTERNAL_ERRORErreur interne du middleware d'authentification

Slugs métier

HTTPerrorChamps additionnels
403CLIENT_TYPE_REQUIREDclientType, contractVersion
403CONTRACT_SIGNATURE_REQUIREDclientType, contractVersion
403PICKUP_ADDRESS_REQUIRED— · réparable par PUT /shipping/pickup-address
400PICKUP_ADDRESS_NOT_GEOCODABLE— · rendu par PUT /shipping/pickup-address
403INSUFFICIENT_BALANCEcurrentBalance, requiredAmount
403ACCOUNT_SUSPENDEDamountDue, paymentStatus, gracePeriodEndsAt
403PAYMENT_REQUIREDamountDue, paymentStatus, gracePeriodEndsAt
409IDEMPOTENCY_KEY_CONFLICT

CLIENT_TYPE_REQUIRED et CONTRACT_SIGNATURE_REQUIRED ne se réparent pas par API : remontez l'alerte à l'opérateur de votre côté — il doit se rendre sur le dashboard. Aucun réessai ne les résout.

PICKUP_ADDRESS_REQUIRED fait exception : ne le traitez pas comme les deux précédents. PUT /shipping/pickup-address le lève, et votre intégration peut donc se débloquer seule. Réserver une alerte humaine à ce slug retarde un colis pour un réglage que votre code sait configurer.

Réponses sans slug

N'attendez pas un champ error systématiquement.

  • 400 de validation (champ requis absent, mauvais type) : { "success": false, "errors": [{ type, msg, path, location }] } — une enveloppe différente : un tableau errors, et aucun champ error. path vous donne le champ fautif (ex. parcelDetails.nature).
  • 400 métier : { "success": false, "message": "..." }message seul. C'est la forme que prennent les refus de créneau, le dépassement de creditLimit en deferred, et — sur POST /rates seulement — weight totalement absent. Le poids non positif n'est plus dans cette liste : 0 et les négatifs sont rejetés par la validation de champ.
  • 404 : { "success": false, "message": "Bordereau non trouvé" }
  • 429 : { "success": false, "message": "...", "window": "minute" } — transitoire : à rejouer, jamais à ignorer. Sans slug error, un client qui branche uniquement sur les slugs et sur status >= 500 le laisse tomber en échec définitif. Classez-le avec les 5xx et rejouez la même clé d'idempotence.
  • 500 : { "success": false, "error": "INTERNAL_ERROR", "message": "..." } — un slug, celui-là, et un message générique : le détail de l'exception reste dans nos journaux. C'est la forme rendue par le contrôle d'accès et par la grande majorité des handlers.
  • 400 sans slug, message brut — propre à POST /rates et POST /shipping, et à eux seuls. Une fois l'authentification passée, ces deux handlers rattrapent toute exception : celle qui ne porte ni slug ni statut propre ressort en 400, avec le message brut de l'exception.
  • 400 VALIDATION_ERROR — propre aux écritures de webhooks (POST /webhooks, PUT /webhooks/:id, DELETE /webhooks/:id) : { "success": false, "error": "VALIDATION_ERROR", "message": "...", "details": "..." }. Le slug y est trompeur : ces handlers rendent VALIDATION_ERROR pour toute exception, y compris une panne interne. details porte le message brut : journalisez-le, ne branchez pas dessus. GET /webhooks, lui, rend un 500 INTERNAL_ERROR normal.

Conséquence : pour ces 400 métier, il n'existe aujourd'hui rien de stable sur quoi brancher — seule la prose distingue « créneau invalide » de « créneau dans le passé ». Ne construisez pas de logique dessus : traitez-les comme des erreurs d'intégration à remonter à un opérateur, avec le message en clair dans vos logs. Les blocages de compte, eux, ont chacun leur slug en 403.

Sur POST /rates et POST /shipping, un 400 ne prouve pas que votre requête était invalide

Ces deux handlers rattrapent toute exception et la rendent en { success: false, message } — 400, sans slug. Une panne interne (base de données injoignable, bug) y prend donc exactement la même forme qu'un créneau invalide : le statut seul ne les distingue pas. Le message est le seul indice, et il n'est pas contractuel.

C'est un comportement connu de ces deux routes. Partout ailleurs, une panne interne sort en 500 INTERNAL_ERROR.

Conséquence pratique : puisqu'un 400 sans slug peut être une panne, ne le traitez pas comme définitif : rejouez-le avec la même clé d'idempotence. Si l'appel avait malgré tout abouti, le rejeu vous rend le bordereau existant en 200 plutôt que d'en créer un second.

Gardez malgré tout la règle status >= 500 ⇒ panne : elle vaut partout, y compris sur ces deux routes. Le contrôle d'accès et le quota s'y exécutent avant le handler.

Exemple de traitement

javascript
const API_KEY = 'sk_live_VOTRE_CLE';
const CLIENT_ID = 'VOTRE_CLIENT_ID';
const BASE_URL = 'https://api.arelpro.com/pro/api/v1';

// 5xx ET 429 sont TRANSITOIRES : votre appelant doit les rejouer. Sur
// POST /shipping, rejouez-les avec la MEME cle d'idempotence.
class ArelRejouable extends Error {}

async function appelArel(path, options = {}) {
  const response = await fetch(`${BASE_URL}${path}`, {
    ...options,
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      'X-Client-Id': CLIENT_ID,
      'Content-Type': 'application/json',
      ...(options.headers || {}),
    },
  });

  // Un 5xx peut venir d'un proxy et n'etre PAS du JSON : ne lisez pas
  // body.error avant d'avoir ecarte ce cas. Reessayable.
  if (response.status >= 500) {
    throw new ArelRejouable(`HTTP ${response.status} — panne serveur, reessayez.`);
  }

  // 429 = quota atteint : TRANSITOIRE, a rejouer — pas un refus de la requete.
  // Il faut l'ecarter ICI : son corps ne porte AUCUN slug `error`, donc le
  // switch ci-dessous le manquerait et il finirait en echec definitif.
  // Pas d'en-tete Retry-After ; X-RateLimit-Reset ne vaut que pour `window`
  // === 'minute'. Sur 'hour' / 'day', requeuez plus tard.
  if (response.status === 429) {
    const quota = await response.json().catch(() => ({}));
    throw new ArelRejouable(`429 — quota ${quota.window ?? 'inconnu'} atteint.`);
  }

  const body = await response.json();

  // 200 ET 201 sont des succes (201 = cree, 200 = rejeu idempotent).
  if (response.ok) return body;

  // Branchez sur le STATUT HTTP et sur body.error (slug stable).
  // Ne branchez JAMAIS sur body.message : c'est de la prose pour un humain.
  switch (body.error) {
    case 'MISSING_API_KEY':
    case 'MISSING_CLIENT_ID':
    case 'INVALID_API_KEY':
    case 'EXPIRED_API_KEY':
      throw new Error('401 — verifiez vos identifiants.');
    case 'INVALID_CLIENT_ID':
      throw new Error("403 — le Client ID n'appartient pas a cette cle.");
    case 'INSUFFICIENT_PERMISSIONS':
      throw new Error("403 — la cle n'a pas le scope requis pour cette methode HTTP.");
    case 'PICKUP_ADDRESS_REQUIRED':
      // Reparable par API : PUT /shipping/pickup-address, puis rejouez.
      // N'alertez un humain que si cet appel echoue a son tour.
      throw new Error(
        "403 — adresse d'enlevement absente : PUT /shipping/pickup-address."
      );
    case 'PICKUP_ADDRESS_NOT_GEOCODABLE':
      // 400 rendu par PUT /shipping/pickup-address lui-meme : adresse
      // introuvable. Renvoyez-la avec coordinates.latitude / .longitude.
      throw new Error('400 — adresse non geocodable, fournissez coordinates.');
    case 'CLIENT_TYPE_REQUIRED':
    case 'CONTRACT_SIGNATURE_REQUIRED':
    case 'ACCOUNT_SUSPENDED':
      // Ceux-la engagent juridiquement ou financierement : action humaine au
      // dashboard, aucun remede par API. Aucun reessai ne les resout.
      throw new Error(`403 — blocage compte (${body.error}), aucun remede par API.`);
    case 'INSUFFICIENT_BALANCE':
      throw new Error(`403 — solde insuffisant : ${body.currentBalance} EUR.`);
    default:
      break;
  }

  // Les erreurs de CHAMP ont une enveloppe differente : un tableau 'errors',
  // et aucun slug 'error'. Testez-la avant de vous rabattre sur le message.
  //   404 => body.message seul  (le 429 est deja parti en ArelRejouable)
  if (Array.isArray(body.errors)) {
    throw new Error(`400 — champs invalides : ${JSON.stringify(body.errors)}`);
  }

  throw new Error(`HTTP ${response.status} : ${body.message}`);
}

Commencer l'intégration

Créez votre compte professionnel et obtenez vos clés API pour démarrer vos premiers envois.

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