Uniship

Branchez Uniship sur votre logiciel

Une API REST pour demander des prix, créer vos envois, récupérer les étiquettes et suivre vos colis, plus des webhooks signés pour être prévenu à chaque changement de statut.

Pour commencer

Le responsable de votre société crée une clé dans le portail : Mon profil → Connexion à votre ERP ou boutique (API). La clé complète n'est affichée qu'une seule fois.

CléUsage
usk_test_…Développement : de vrais prix, mais aucun envoi réel et aucune facturation.
usk_live_…Envois réels, possibles dès l'ouverture d'Uniship (début décembre 2026). Avant, la création d'envoi répond 409 portal_closed.

Adresse de base : https://portail.uniship.be/api/v1. Authentification : en-tête Authorization: Bearer <votre clé>.

curl https://portail.uniship.be/api/v1/ping \
  -H "Authorization: Bearer usk_test_…"

1. Demander un prix

Chaque offre est valable 15 minutes et ne sert qu'à un seul envoi. Les prix sont en euros, hors TVA, carburant inclus.

curl -X POST https://portail.uniship.be/api/v1/offers \
  -H "Authorization: Bearer usk_test_…" -H "Content-Type: application/json" \
  -d '{
    "originCountry": "BE", "originZip": "7170",
    "destCountry": "FR",   "destZip": "75001",
    "kind": "parcel",
    "parcels": [ { "weight": 5, "length": 40, "width": 30, "height": 20 } ]
  }'

La réponse contient la liste offers : offerId, carrier, label, price, transitMin / transitMax (jours ouvrés), les suppléments éventuels (fees) et les options de livraison (options).

2. Créer l'envoi

curl -X POST https://portail.uniship.be/api/v1/shipments \
  -H "Authorization: Bearer usk_test_…" -H "Content-Type: application/json" \
  -H "Idempotency-Key: CMD-1001" \
  -d '{
    "offerId": "…", "optionId": "none", "reference": "CMD-1001",
    "sender":    { "name": "Ma société", "address": "Rue de l'Exemple 1", "zip": "7170", "city": "Manage", "country": "BE", "phone": "…", "email": "…" },
    "recipient": { "name": "Marie Martin", "address": "12 rue de Rivoli", "zip": "75001", "city": "Paris", "country": "FR" }
  }'

Avec Idempotency-Key, un nouvel essai après une coupure réseau renvoie le même envoi sans en créer un second (valable 48 h). Hors Union européenne, ajoutez l'objet customs (nature, valeur et origine des marchandises, numéros fiscaux).

3. Étiquette et suivi

# Détail et suivi
curl https://portail.uniship.be/api/v1/shipments/{id} -H "Authorization: Bearer …"
# Étiquette en PDF
curl "https://portail.uniship.be/api/v1/shipments/{id}/label?format=pdf" -H "Authorization: Bearer …" -o etiquette.pdf

Tant que l'étiquette n'est pas prête, la réponse est 409 : réessayez un peu plus tard, ou attendez le webhook.

Toutes les routes

Méthode et cheminRôle
GET /v1/pingTester la clé
POST /v1/offersDemander des prix (30 par minute)
POST /v1/shipmentsCréer un envoi depuis une offre (60 par minute)
GET /v1/shipmentsLister vos envois ; filtres status, reference ; pagination startAfter
GET /v1/shipments/{id}Lire un envoi et son suivi
GET /v1/shipments/{id}/labelÉtiquette (JSON base64 ou PDF)

Lecture : 120 requêtes par minute et par clé. En cas de dépassement : 429 avec l'en-tête Retry-After. Erreurs : {"error":{"code":"…","message":"…"}}.

Statuts : pending, label_generated, picked_up, in_transit, out_for_delivery, delivered, exception, cancelled.

Télécharger la spécification OpenAPI (importable dans Postman, Insomnia ou Swagger).

Webhooks

Déclarez une adresse HTTPS dans le portail (Mon profil → Notifications automatiques). Uniship envoie un POST JSON à chaque création d'envoi (shipment.created) et à chaque changement de statut (shipment.status_changed).

{ "id": "evt_…", "type": "shipment.status_changed", "createdAt": "2026-12-01T09:30:00.000Z",
  "data": { "id": "…", "reference": "CMD-1001", "status": "in_transit", "previousStatus": "picked_up", "trackingNumber": "…" } }

Vérifier la signature

En-tête Uniship-Signature: t=<secondes>,v1=<hex>, où v1 est le HMAC-SHA256 de "<t>.<corps brut>" avec votre secret. Refusez un message dont t date de plus de 5 minutes.

const crypto = require('crypto');
function verify(rawBody, header, secret) {
  const p = Object.fromEntries(header.split(',').map(x => x.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
  const exp = crypto.createHmac('sha256', secret).update(p.t + '.' + rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(exp), Buffer.from(p.v1));
}

Sécurité et bonnes pratiques