Guide d'integration complet
Ce guide couvre tout ce dont vous avez besoin pour integrer les services de livraison Dropcolis dans votre application. De l'authentification aux webhooks, vous trouverez des exemples detailles dans plusieurs langages de programmation.
Demarrage rapide
Commencez en 5 minutes
- Obtenir les cles API: Connectez-vous a votre Tableau de bord partenaire et creez des cles API
- Installer les dependances: Ajouter la bibliotheque HMAC pour la generation de signature
- Effectuez votre premier appel: Testez avec une demande de devis
- Creer des commandes: Commencez a creer des commandes de livraison
Identifiants de test
Utilisez ces identifiants sandbox pour le developpement:
- Cle publique:
pk_test_dropcolis_partner - Cle secrete:
sk_test_secret_dropcolis_partner - URL de base:
http://localhost:5001/v1
Authentication
Toutes les requetes API necessitent une authentification HMAC-SHA256. Vous devez inclure trois en-tetes avec chaque requete:
| En-tete | Description | Exemple |
|---|---|---|
X-Api-Key |
Votre cle API publique | pk_test_dropcolis_partner |
X-Timestamp |
Horodatage Unix (secondes depuis l'epoque) | 1702051200 |
X-Signature |
Signature HMAC-SHA256 avec prefixe v1= | v1=abc123... |
Generation de signature
La signature protege vos requetes contre la falsification. Voici comment la generer:
// Step 1: Build the string to sign
string_to_sign = timestamp + "\n" + http_method + "\n" + path + "\n" + body
// Step 2: Create HMAC-SHA256 signature
signature = HMAC_SHA256(secret_key, string_to_sign)
// Step 3: Format the header
X-Signature = "v1=" + hex_encode(signature)
Classes d'aide a l'authentification
import hmac
import hashlib
import time
import json
import uuid
import requests
class DropcolisClient:
"""Dropcolis API Client with HMAC authentication"""
def __init__(self, public_key, secret_key, base_url="http://localhost:5001"):
self.public_key = public_key
self.secret_key = secret_key
self.base_url = base_url
def _generate_signature(self, method, path, body, timestamp):
"""Generate HMAC-SHA256 signature"""
string_to_sign = f"{timestamp}\n{method}\n{path}\n{body}"
signature = hmac.new(
self.secret_key.encode('utf-8'),
string_to_sign.encode('utf-8'),
hashlib.sha256
).hexdigest()
return f"v1={signature}"
def _make_request(self, method, path, data=None, idempotency_key=None):
"""Make authenticated API request"""
timestamp = str(int(time.time()))
body = json.dumps(data, separators=(',', ':')) if data else ""
headers = {
'Content-Type': 'application/json',
'X-Api-Key': self.public_key,
'X-Timestamp': timestamp,
'X-Signature': self._generate_signature(method, path, body, timestamp)
}
if idempotency_key:
headers['Idempotency-Key'] = idempotency_key
url = f"{self.base_url}{path}"
if method == 'GET':
response = requests.get(url, headers=headers)
elif method == 'POST':
response = requests.post(url, headers=headers, data=body)
elif method == 'DELETE':
response = requests.delete(url, headers=headers)
return response.json(), response.status_code
def get_rate_quote(self, quote_data):
"""Get delivery rate quote"""
return self._make_request('POST', '/v1/rates/quote', quote_data)
def create_order(self, order_data):
"""Create a new delivery order"""
idempotency_key = str(uuid.uuid4())
return self._make_request('POST', '/v1/orders', order_data, idempotency_key)
def get_order(self, order_id):
"""Get order details"""
return self._make_request('GET', f'/v1/orders/{order_id}')
def cancel_order(self, order_id, reason=None):
"""Cancel an order"""
data = {'reason': reason} if reason else {}
return self._make_request('POST', f'/v1/orders/{order_id}/cancel', data)
# Usage Example
if __name__ == "__main__":
client = DropcolisClient(
public_key="pk_test_dropcolis_partner",
secret_key="sk_test_secret_dropcolis_partner"
)
# Get a rate quote
quote, status = client.get_rate_quote({
"service_level": "standard",
"pickup": {
"address": {
"postal_code": "H1A1A1",
"city": "Montreal",
"province": "QC",
"country": "CA"
}
},
"dropoff": {
"address": {
"postal_code": "H2L1P1",
"city": "Montreal",
"province": "QC",
"country": "CA"
}
},
"parcels": [{"weight_kg": 2.5}]
})
print(f"Rate: ${quote.get('rate_cents', 0) / 100:.2f} CAD")
const crypto = require('crypto');
const axios = require('axios');
class DropcolisClient {
constructor(publicKey, secretKey, baseUrl = 'http://localhost:5001') {
this.publicKey = publicKey;
this.secretKey = secretKey;
this.baseUrl = baseUrl;
}
_generateSignature(method, path, body, timestamp) {
const stringToSign = `${timestamp}\n${method}\n${path}\n${body}`;
const signature = crypto
.createHmac('sha256', this.secretKey)
.update(stringToSign)
.digest('hex');
return `v1=${signature}`;
}
async _makeRequest(method, path, data = null, idempotencyKey = null) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = data ? JSON.stringify(data) : '';
const headers = {
'Content-Type': 'application/json',
'X-Api-Key': this.publicKey,
'X-Timestamp': timestamp,
'X-Signature': this._generateSignature(method, path, body, timestamp)
};
if (idempotencyKey) {
headers['Idempotency-Key'] = idempotencyKey;
}
const config = { headers };
const url = `${this.baseUrl}${path}`;
try {
let response;
if (method === 'GET') {
response = await axios.get(url, config);
} else if (method === 'POST') {
response = await axios.post(url, data, config);
} else if (method === 'DELETE') {
response = await axios.delete(url, config);
}
return { data: response.data, status: response.status };
} catch (error) {
return {
data: error.response?.data || { error: error.message },
status: error.response?.status || 500
};
}
}
async getRateQuote(quoteData) {
return this._makeRequest('POST', '/v1/rates/quote', quoteData);
}
async createOrder(orderData) {
const idempotencyKey = crypto.randomUUID();
return this._makeRequest('POST', '/v1/orders', orderData, idempotencyKey);
}
async getOrder(orderId) {
return this._makeRequest('GET', `/v1/orders/${orderId}`);
}
async cancelOrder(orderId, reason = null) {
const data = reason ? { reason } : {};
return this._makeRequest('POST', `/v1/orders/${orderId}/cancel`, data);
}
}
// Usage Example
async function main() {
const client = new DropcolisClient(
'pk_test_dropcolis_partner',
'sk_test_secret_dropcolis_partner'
);
const { data, status } = await client.getRateQuote({
service_level: 'standard',
pickup: {
address: { postal_code: 'H1A1A1', city: 'Montreal', province: 'QC', country: 'CA' }
},
dropoff: {
address: { postal_code: 'H2L1P1', city: 'Montreal', province: 'QC', country: 'CA' }
},
parcels: [{ weight_kg: 2.5 }]
});
console.log(`Rate: $${(data.rate_cents / 100).toFixed(2)} CAD`);
}
module.exports = DropcolisClient;
<?php
class DropcolisClient {
private $publicKey;
private $secretKey;
private $baseUrl;
public function __construct($publicKey, $secretKey, $baseUrl = 'http://localhost:5001') {
$this->publicKey = $publicKey;
$this->secretKey = $secretKey;
$this->baseUrl = $baseUrl;
}
private function generateSignature($method, $path, $body, $timestamp) {
$stringToSign = "{$timestamp}\n{$method}\n{$path}\n{$body}";
$signature = hash_hmac('sha256', $stringToSign, $this->secretKey);
return "v1={$signature}";
}
private function makeRequest($method, $path, $data = null, $idempotencyKey = null) {
$timestamp = (string) time();
$body = $data ? json_encode($data, JSON_UNESCAPED_SLASHES) : '';
$headers = [
'Content-Type: application/json',
'X-Api-Key: ' . $this->publicKey,
'X-Timestamp: ' . $timestamp,
'X-Signature: ' . $this->generateSignature($method, $path, $body, $timestamp)
];
if ($idempotencyKey) {
$headers[] = 'Idempotency-Key: ' . $idempotencyKey;
}
$ch = curl_init($this->baseUrl . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method === 'DELETE') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
}
$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'data' => json_decode($response, true),
'status' => $statusCode
];
}
public function getRateQuote($quoteData) {
return $this->makeRequest('POST', '/v1/rates/quote', $quoteData);
}
public function createOrder($orderData) {
$idempotencyKey = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
return $this->makeRequest('POST', '/v1/orders', $orderData, $idempotencyKey);
}
public function getOrder($orderId) {
return $this->makeRequest('GET', "/v1/orders/{$orderId}");
}
public function cancelOrder($orderId, $reason = null) {
$data = $reason ? ['reason' => $reason] : [];
return $this->makeRequest('POST', "/v1/orders/{$orderId}/cancel", $data);
}
}
// Usage Example
$client = new DropcolisClient('pk_test_dropcolis_partner', 'sk_test_secret_dropcolis_partner');
$result = $client->getRateQuote([
'service_level' => 'standard',
'pickup' => ['address' => ['postal_code' => 'H1A1A1', 'city' => 'Montreal', 'province' => 'QC', 'country' => 'CA']],
'dropoff' => ['address' => ['postal_code' => 'H2L1P1', 'city' => 'Montreal', 'province' => 'QC', 'country' => 'CA']],
'parcels' => [['weight_kg' => 2.5]]
]);
echo "Rate: $" . number_format($result['data']['rate_cents'] / 100, 2) . " CAD\n";
?>
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.http.*;
import java.net.URI;
import java.time.Instant;
import java.util.UUID;
public class DropcolisClient {
private final String publicKey;
private final String secretKey;
private final String baseUrl;
private final HttpClient httpClient;
public DropcolisClient(String publicKey, String secretKey) {
this(publicKey, secretKey, "http://localhost:5001");
}
public DropcolisClient(String publicKey, String secretKey, String baseUrl) {
this.publicKey = publicKey;
this.secretKey = secretKey;
this.baseUrl = baseUrl;
this.httpClient = HttpClient.newHttpClient();
}
private String generateSignature(String method, String path, String body, String timestamp)
throws Exception {
String stringToSign = timestamp + "\n" + method + "\n" + path + "\n" + body;
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256");
mac.init(secretKeySpec);
byte[] hmacBytes = mac.doFinal(stringToSign.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : hmacBytes) {
hexString.append(String.format("%02x", b));
}
return "v1=" + hexString.toString();
}
public HttpResponse<String> makeRequest(String method, String path, String body,
String idempotencyKey) throws Exception {
String timestamp = String.valueOf(Instant.now().getEpochSecond());
String signature = generateSignature(method, path, body != null ? body : "", timestamp);
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + path))
.header("Content-Type", "application/json")
.header("X-Api-Key", publicKey)
.header("X-Timestamp", timestamp)
.header("X-Signature", signature);
if (idempotencyKey != null) {
requestBuilder.header("Idempotency-Key", idempotencyKey);
}
if ("POST".equals(method)) {
requestBuilder.POST(HttpRequest.BodyPublishers.ofString(body != null ? body : ""));
} else if ("GET".equals(method)) {
requestBuilder.GET();
} else if ("DELETE".equals(method)) {
requestBuilder.DELETE();
}
return httpClient.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
}
public HttpResponse<String> createOrder(String orderJson) throws Exception {
String idempotencyKey = UUID.randomUUID().toString();
return makeRequest("POST", "/v1/orders", orderJson, idempotencyKey);
}
public HttpResponse<String> getOrder(String orderId) throws Exception {
return makeRequest("GET", "/v1/orders/" + orderId, null, null);
}
// Usage
public static void main(String[] args) throws Exception {
DropcolisClient client = new DropcolisClient(
"pk_test_dropcolis_partner",
"sk_test_secret_dropcolis_partner"
);
String orderJson = """
{
"partner_order_id": "JAVA-001",
"service_level": "standard",
"pickup": { ... },
"dropoff": { ... },
"parcels": [{ "weight_kg": 2.5 }]
}
""";
HttpResponse<String> response = client.createOrder(orderJson);
System.out.println("Status: " + response.statusCode());
System.out.println("Body: " + response.body());
}
}
Apercu de l'API
L'API Dropcolis suit les conventions RESTful. Tous les points de terminaison sont prefixes par /v1.
Points de terminaison disponibles
Obtenir un devis de livraison avant de creer une commande
Creer une nouvelle commande de livraison
Recuperer les details de la commande et le statut actuel
Lister toutes les commandes avec filtres optionnels
Annuler une commande en attente
Envoyer un webhook de test a votre point de terminaison
Obtenir des devis
Avant de creer une commande, vous pouvez obtenir un devis pour afficher les tarifs a vos clients.
quote_data = {
"service_level": "standard", # standard, express, or same_day
"pickup": {
"address": {
"line1": "123 Main Street",
"city": "Montreal",
"province": "QC",
"postal_code": "H1A1A1",
"country": "CA"
}
},
"dropoff": {
"address": {
"line1": "456 Oak Avenue",
"city": "Montreal",
"province": "QC",
"postal_code": "H2L1P1",
"country": "CA"
}
},
"parcels": [
{
"weight_kg": 2.5,
"length_cm": 30,
"width_cm": 20,
"height_cm": 15
}
]
}
result, status = client.get_rate_quote(quote_data)
if status == 200:
print(f"Service: {result['service_level']}")
print(f"Rate: ${result['rate_cents'] / 100:.2f} {result['currency']}")
print(f"Estimated Delivery: {result['estimated_delivery']}")
print(f"Transit Days: {result['transit_days']}")
const quoteData = {
service_level: 'express',
pickup: {
address: {
line1: '123 Main Street',
city: 'Montreal',
province: 'QC',
postal_code: 'H1A1A1',
country: 'CA'
}
},
dropoff: {
address: {
line1: '456 Oak Avenue',
city: 'Montreal',
province: 'QC',
postal_code: 'H2L1P1',
country: 'CA'
}
},
parcels: [{ weight_kg: 2.5, length_cm: 30, width_cm: 20, height_cm: 15 }]
};
const { data, status } = await client.getRateQuote(quoteData);
if (status === 200) {
console.log(`Service: ${data.service_level}`);
console.log(`Rate: $${(data.rate_cents / 100).toFixed(2)} ${data.currency}`);
console.log(`Estimated Delivery: ${data.estimated_delivery}`);
}
// Java
Map<String, Object> pickup = Map.of(
"address", Map.of(
"line1", "123 Main Street",
"city", "Montreal",
"province", "QC",
"postal_code", "H1A1A1",
"country", "CA"
)
);
Map<String, Object> dropoff = Map.of(
"address", Map.of(
"line1", "456 Oak Avenue",
"city", "Montreal",
"province", "QC",
"postal_code", "H2L1P1",
"country", "CA"
)
);
List<Map<String, Object>> parcels = List.of(
Map.of("weight_kg", 2.5, "length_cm", 30, "width_cm", 20, "height_cm", 15)
);
Map<String, Object> quoteData = Map.of(
"service_level", "standard",
"pickup", pickup,
"dropoff", dropoff,
"parcels", parcels
);
Map<String, Object> result = client.getRateQuote(quoteData);
System.out.println("Service: " + result.get("service_level"));
System.out.println("Rate: $" + ((Integer) result.get("rate_cents") / 100.0) + " " + result.get("currency"));
System.out.println("Estimated Delivery: " + result.get("estimated_delivery"));
System.out.println("Transit Days: " + result.get("transit_days"));
// PHP
$quoteData = [
'service_level' => 'standard',
'pickup' => [
'address' => [
'line1' => '123 Main Street',
'city' => 'Montreal',
'province' => 'QC',
'postal_code' => 'H1A1A1',
'country' => 'CA'
]
],
'dropoff' => [
'address' => [
'line1' => '456 Oak Avenue',
'city' => 'Montreal',
'province' => 'QC',
'postal_code' => 'H2L1P1',
'country' => 'CA'
]
],
'parcels' => [
['weight_kg' => 2.5, 'length_cm' => 30, 'width_cm' => 20, 'height_cm' => 15]
]
];
$result = $client->getRateQuote($quoteData);
echo "Service: " . $result['service_level'] . "\n";
echo "Rate: $" . number_format($result['rate_cents'] / 100, 2) . " " . $result['currency'] . "\n";
echo "Estimated Delivery: " . $result['estimated_delivery'] . "\n";
echo "Transit Days: " . $result['transit_days'] . "\n";
curl -X POST "http://localhost:5001/v1/rates/quote" \
-H "Content-Type: application/json" \
-H "X-Api-Key: pk_test_dropcolis_partner" \
-H "X-Timestamp: $(date +%s)" \
-H "X-Signature: v1=YOUR_SIGNATURE" \
-d '{
"service_level": "standard",
"pickup": {"address": {"postal_code": "H1A1A1", "city": "Montreal", "province": "QC", "country": "CA"}},
"dropoff": {"address": {"postal_code": "H2L1P1", "city": "Montreal", "province": "QC", "country": "CA"}},
"parcels": [{"weight_kg": 2.5}]
}'
Response
{
"quote_id": "qt_abc123def456",
"service_level": "standard",
"rate_cents": 1299,
"currency": "CAD",
"estimated_delivery": "2025-11-10T17:00:00-05:00",
"transit_days": 2,
"valid_until": "2025-11-08T23:59:59Z"
}
Creer des commandes
Creez des commandes de livraison avec les details de ramassage et de livraison, les informations sur les colis et les preferences optionnelles.
Champs requis
| Champ | Type | Description |
|---|---|---|
partner_order_id | string | Votre identifiant de commande unique |
service_level | string | standard, express, ou same_day |
pickup | object | Lieu de ramassage avec nom, telephone, courriel, adresse, fenetre horaire |
dropoff | object | Lieu de livraison avec la meme structure que le ramassage |
parcels | array | 1-50 colis avec dimensions, poids et valeur |
Exemple de commande complete
order_data = {
"partner_order_id": "ORD-2025-001234",
"reference": "Invoice #55555",
"service_level": "standard",
# Cash on delivery (optional)
"cod": {
"amount_cents": 0, # Set > 0 for COD
"currency": "CAD"
},
# Insurance coverage (optional)
"insurance": {
"amount_cents": 15000, # $150.00 coverage
"currency": "CAD"
},
# Pickup details
"pickup": {
"name": "Warehouse Montreal",
"phone": "+1-514-555-0001",
"email": "warehouse@yourcompany.com",
"address": {
"line1": "123 Industrial Blvd",
"line2": "Unit 5",
"city": "Montreal",
"province": "QC",
"postal_code": "H1A1A1",
"country": "CA"
},
"time_window": {
"start": "2025-11-08T09:00:00-05:00",
"end": "2025-11-08T12:00:00-05:00"
},
"instructions": "Loading dock at rear entrance"
},
# Delivery details
"dropoff": {
"name": "John Smith",
"phone": "+1-438-555-0002",
"email": "john.smith@customer.com",
"address": {
"line1": "456 Residential Street",
"line2": "Apt 302",
"city": "Montreal",
"province": "QC",
"postal_code": "H2L1P1",
"country": "CA"
},
"time_window": {
"start": "2025-11-08T14:00:00-05:00",
"end": "2025-11-08T18:00:00-05:00"
},
"instructions": "Leave with concierge if not home"
},
# Parcels
"parcels": [
{
"parcel_id": "PKG-001",
"description": "Electronics - Laptop",
"length_cm": 40,
"width_cm": 30,
"height_cm": 10,
"weight_kg": 3.2,
"value_cents": 150000, # $1500 declared value
"fragile": True,
"barcode": "1234567890"
},
{
"parcel_id": "PKG-002",
"description": "Accessories",
"length_cm": 20,
"width_cm": 15,
"height_cm": 10,
"weight_kg": 0.5,
"value_cents": 5000
}
],
# General instructions
"instructions": "Handle with care - fragile electronics",
# Custom metadata (stored but not processed)
"metadata": {
"customer_id": "CUST-12345",
"order_source": "website",
"priority": "high"
}
}
result, status = client.create_order(order_data)
if status == 201:
print(f"Order created successfully!")
print(f"Order ID: {result['order_id']}")
print(f"Tracking URL: {result['tracking_url']}")
print(f"Status: {result['status']}")
else:
print(f"Error: {result.get('error', {}).get('message')}")
const orderData = {
partner_order_id: 'ORD-2025-001234',
reference: 'Invoice #55555',
service_level: 'standard',
cod: { amount_cents: 0, currency: 'CAD' },
insurance: { amount_cents: 15000, currency: 'CAD' },
pickup: {
name: 'Warehouse Montreal',
phone: '+1-514-555-0001',
email: 'warehouse@yourcompany.com',
address: {
line1: '123 Industrial Blvd',
line2: 'Unit 5',
city: 'Montreal',
province: 'QC',
postal_code: 'H1A1A1',
country: 'CA'
},
time_window: {
start: '2025-11-08T09:00:00-05:00',
end: '2025-11-08T12:00:00-05:00'
},
instructions: 'Loading dock at rear entrance'
},
dropoff: {
name: 'John Smith',
phone: '+1-438-555-0002',
email: 'john.smith@customer.com',
address: {
line1: '456 Residential Street',
line2: 'Apt 302',
city: 'Montreal',
province: 'QC',
postal_code: 'H2L1P1',
country: 'CA'
},
time_window: {
start: '2025-11-08T14:00:00-05:00',
end: '2025-11-08T18:00:00-05:00'
},
instructions: 'Leave with concierge if not home'
},
parcels: [{
parcel_id: 'PKG-001',
description: 'Electronics - Laptop',
length_cm: 40, width_cm: 30, height_cm: 10,
weight_kg: 3.2,
value_cents: 150000,
fragile: true,
barcode: '1234567890'
}],
instructions: 'Handle with care - fragile electronics',
metadata: { customer_id: 'CUST-12345', order_source: 'website' }
};
const { data, status } = await client.createOrder(orderData);
if (status === 201) {
console.log('Order created!');
console.log(`Order ID: ${data.order_id}`);
console.log(`Tracking: ${data.tracking_url}`);
} else {
console.error(`Error: ${data.error?.message}`);
}
// Java - Create Order
Map<String, Object> pickup = new HashMap<>();
pickup.put("name", "Warehouse Montreal");
pickup.put("phone", "+1-514-555-0001");
pickup.put("email", "warehouse@yourcompany.com");
pickup.put("address", Map.of(
"line1", "123 Industrial Blvd",
"line2", "Unit 5",
"city", "Montreal",
"province", "QC",
"postal_code", "H1A1A1",
"country", "CA"
));
pickup.put("time_window", Map.of(
"start", "2025-11-08T09:00:00-05:00",
"end", "2025-11-08T12:00:00-05:00"
));
pickup.put("instructions", "Loading dock at rear entrance");
Map<String, Object> dropoff = new HashMap<>();
dropoff.put("name", "John Smith");
dropoff.put("phone", "+1-438-555-0002");
dropoff.put("email", "john.smith@customer.com");
dropoff.put("address", Map.of(
"line1", "456 Residential Street",
"line2", "Apt 302",
"city", "Montreal",
"province", "QC",
"postal_code", "H2L1P1",
"country", "CA"
));
dropoff.put("time_window", Map.of(
"start", "2025-11-08T14:00:00-05:00",
"end", "2025-11-08T18:00:00-05:00"
));
dropoff.put("instructions", "Leave with concierge if not home");
List<Map<String, Object>> parcels = List.of(
Map.of(
"parcel_id", "PKG-001",
"description", "Electronics - Laptop",
"length_cm", 40, "width_cm", 30, "height_cm", 10,
"weight_kg", 3.2,
"value_cents", 150000,
"fragile", true,
"barcode", "1234567890"
)
);
Map<String, Object> orderData = new HashMap<>();
orderData.put("partner_order_id", "ORD-2025-001234");
orderData.put("reference", "Invoice #55555");
orderData.put("service_level", "standard");
orderData.put("cod", Map.of("amount_cents", 0, "currency", "CAD"));
orderData.put("insurance", Map.of("amount_cents", 15000, "currency", "CAD"));
orderData.put("pickup", pickup);
orderData.put("dropoff", dropoff);
orderData.put("parcels", parcels);
orderData.put("instructions", "Handle with care - fragile electronics");
Map<String, Object> result = client.createOrder(orderData);
System.out.println("Order created successfully!");
System.out.println("Order ID: " + result.get("order_id"));
System.out.println("Tracking URL: " + result.get("tracking_url"));
System.out.println("Status: " + result.get("status"));
// PHP - Create Order
$orderData = [
'partner_order_id' => 'ORD-2025-001234',
'reference' => 'Invoice #55555',
'service_level' => 'standard',
'cod' => ['amount_cents' => 0, 'currency' => 'CAD'],
'insurance' => ['amount_cents' => 15000, 'currency' => 'CAD'],
'pickup' => [
'name' => 'Warehouse Montreal',
'phone' => '+1-514-555-0001',
'email' => 'warehouse@yourcompany.com',
'address' => [
'line1' => '123 Industrial Blvd',
'line2' => 'Unit 5',
'city' => 'Montreal',
'province' => 'QC',
'postal_code' => 'H1A1A1',
'country' => 'CA'
],
'time_window' => [
'start' => '2025-11-08T09:00:00-05:00',
'end' => '2025-11-08T12:00:00-05:00'
],
'instructions' => 'Loading dock at rear entrance'
],
'dropoff' => [
'name' => 'John Smith',
'phone' => '+1-438-555-0002',
'email' => 'john.smith@customer.com',
'address' => [
'line1' => '456 Residential Street',
'line2' => 'Apt 302',
'city' => 'Montreal',
'province' => 'QC',
'postal_code' => 'H2L1P1',
'country' => 'CA'
],
'time_window' => [
'start' => '2025-11-08T14:00:00-05:00',
'end' => '2025-11-08T18:00:00-05:00'
],
'instructions' => 'Leave with concierge if not home'
],
'parcels' => [
[
'parcel_id' => 'PKG-001',
'description' => 'Electronics - Laptop',
'length_cm' => 40,
'width_cm' => 30,
'height_cm' => 10,
'weight_kg' => 3.2,
'value_cents' => 150000,
'fragile' => true,
'barcode' => '1234567890'
]
],
'instructions' => 'Handle with care - fragile electronics',
'metadata' => ['customer_id' => 'CUST-12345', 'order_source' => 'website']
];
$result = $client->createOrder($orderData);
if (isset($result['order_id'])) {
echo "Order created successfully!\n";
echo "Order ID: " . $result['order_id'] . "\n";
echo "Tracking URL: " . $result['tracking_url'] . "\n";
echo "Status: " . $result['status'] . "\n";
} else {
echo "Error: " . $result['error']['message'] . "\n";
}
Response
{
"order_id": "dc_9f7a1f2e3d4c5b6a",
"partner_order_id": "ORD-2025-001234",
"status": "pending",
"service_level": "standard",
"tracking_url": "http://localhost:5001/track/dc_9f7a1f2e3d4c5b6a",
"pickup": {
"name": "Warehouse Montreal",
"phone": "+1-514-555-0001",
"email": "warehouse@yourcompany.com",
"address": {
"street": "100 Rue Saint-Paul",
"city": "Montreal",
"province": "QC",
"postal_code": "H2Y1Z3",
"country": "CA"
},
"time_window": {
"start": "2025-11-08T09:00:00-05:00",
"end": "2025-11-08T12:00:00-05:00"
}
},
"dropoff": {
"name": "Jean Tremblay",
"phone": "+1-514-555-1234",
"email": "jean.tremblay@email.com",
"address": {
"street": "456 Boulevard Rosemont",
"unit": "Apt 302",
"city": "Montreal",
"province": "QC",
"postal_code": "H2S2K1",
"country": "CA"
},
"time_window": {
"start": "2025-11-08T14:00:00-05:00",
"end": "2025-11-08T18:00:00-05:00"
}
},
"parcels": [
{
"parcel_id": "prc_abc123",
"description": "MacBook Pro 16-inch",
"weight_kg": 2.5,
"dimensions": {
"length_cm": 40,
"width_cm": 30,
"height_cm": 10
},
"value_cents": 299900,
"currency": "CAD"
}
],
"rate_cents": 1499,
"currency": "CAD",
"estimated_delivery": "2025-11-08T18:00:00-05:00",
"created_at": "2025-11-08T08:30:00-05:00",
"updated_at": "2025-11-08T08:30:00-05:00"
}
Suivre les commandes
Recuperez le statut actuel et l'historique complet de toute commande.
# Python
result, status = client.get_order("dc_9f7a1f2e3d4c5b6a")
print(f"Status: {result['status']}")
print(f"Last Update: {result['updated_at']}")
# Status history
for event in result.get('status_history', []):
print(f" {event['timestamp']}: {event['status']}")
// JavaScript
const { data, status } = await client.getOrder("dc_9f7a1f2e3d4c5b6a");
console.log(`Status: ${data.status}`);
console.log(`Last Update: ${data.updated_at}`);
// Status history
if (data.status_history) {
data.status_history.forEach(event => {
console.log(` ${event.timestamp}: ${event.status}`);
});
}
// Java
Map<String, Object> result = client.getOrder("dc_9f7a1f2e3d4c5b6a");
System.out.println("Status: " + result.get("status"));
System.out.println("Last Update: " + result.get("updated_at"));
// Status history
List<Map<String, Object>> history =
(List<Map<String, Object>>) result.get("status_history");
if (history != null) {
for (Map<String, Object> event : history) {
System.out.println(" " + event.get("timestamp") + ": " + event.get("status"));
}
}
// PHP
$result = $client->getOrder("dc_9f7a1f2e3d4c5b6a");
echo "Status: " . $result['status'] . "\n";
echo "Last Update: " . $result['updated_at'] . "\n";
// Status history
if (isset($result['status_history'])) {
foreach ($result['status_history'] as $event) {
echo " " . $event['timestamp'] . ": " . $event['status'] . "\n";
}
}
Statuts des commandes
| Statut | Description |
|---|---|
pending | Commande recue, en attente de traitement |
accepted | Commande acceptee par Dropcolis |
courier_assigned | Coursier assigne |
picked_up | Colis ramasse a l'origine |
in_transit | Colis en transit |
out_for_delivery | En cours de livraison finale |
delivered | Livre avec succes |
failed | Tentative de livraison echouee |
cancelled | Commande annulee |
Page de suivi publique
Partagez le tracking_url avec vos clients. Ils peuvent suivre leur livraison sans authentification:
http://localhost:5001/track/dc_9f7a1f2e3d4c5b6a
Annuler les commandes
Annulez une commande avant qu'elle ne soit ramassee. Les commandes deja en transit ne peuvent pas etre annulees.
# Python
result, status = client.cancel_order(
order_id="dc_9f7a1f2e3d4c5b6a",
reason="Customer requested cancellation"
)
if status == 200:
print("Order cancelled successfully")
else:
print(f"Cannot cancel: {result['error']['message']}")
// JavaScript
try {
const { data, status } = await client.cancelOrder(
"dc_9f7a1f2e3d4c5b6a",
"Customer requested cancellation"
);
if (status === 200) {
console.log("Order cancelled successfully");
}
} catch (error) {
console.log(`Cannot cancel: ${error.response.data.error.message}`);
}
// Java
try {
Map<String, Object> result = client.cancelOrder(
"dc_9f7a1f2e3d4c5b6a",
"Customer requested cancellation"
);
System.out.println("Order cancelled successfully");
} catch (ApiException e) {
System.out.println("Cannot cancel: " + e.getMessage());
}
// PHP
$result = $client->cancelOrder(
"dc_9f7a1f2e3d4c5b6a",
"Customer requested cancellation"
);
if (isset($result['status']) && $result['status'] === 'cancelled') {
echo "Order cancelled successfully\n";
} else {
echo "Cannot cancel: " . $result['error']['message'] . "\n";
}
pending or accepted status.
Once a courier is assigned or the package is picked up, cancellation is not possible.
Configuration des webhooks
Les webhooks notifient votre systeme en temps reel lorsque le statut d'une commande change. Au lieu d'interroger l'API, vous recevez des notifications instantanees.
Configuration de votre point de terminaison webhook
- Creez un point de terminaison HTTPS sur votre serveur pour recevoir les evenements webhook
- Configurez votre URL webhook dans le tableau de bord partenaire
- Verifiez les signatures webhook pour garantir l'authenticite
- Repondez avec 200 OK pour accuser reception
Exemple de gestionnaire webhook
from flask import Flask, request, jsonify
import hmac
import hashlib
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret"
def verify_signature(payload, signature, secret):
"""Verify the webhook signature"""
expected = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
# Extract signature value (format: "v1=signature")
if signature.startswith('v1='):
signature = signature[3:]
return hmac.compare_digest(expected, signature)
@app.route('/webhooks/dropcolis', methods=['POST'])
def handle_webhook():
# Get signature from header
signature = request.headers.get('Drop-Signature', '')
# Verify signature
if not verify_signature(request.data, signature, WEBHOOK_SECRET):
return jsonify({'error': 'Invalid signature'}), 401
# Parse event
event = request.json
event_type = event.get('type')
event_data = event.get('data', {})
# Handle different event types
if event_type == 'order.accepted':
print(f"Order {event_data['order_id']} accepted")
# Update your order status in database
elif event_type == 'courier.assigned':
courier = event_data.get('courier', {})
print(f"Courier {courier.get('name')} assigned")
# Notify customer about courier assignment
elif event_type == 'order.picked_up':
print(f"Order {event_data['order_id']} picked up")
# Send SMS/email notification
elif event_type == 'order.out_for_delivery':
print(f"Order out for delivery, ETA: {event_data.get('eta')}")
# Send "out for delivery" notification
elif event_type == 'order.delivered':
print(f"Order {event_data['order_id']} delivered!")
# Mark order as complete, send confirmation
elif event_type == 'order.failed':
reason = event_data.get('failure_reason')
print(f"Delivery failed: {reason}")
# Handle failed delivery, contact customer
# Always return 200 to acknowledge receipt
return jsonify({'received': True}), 200
if __name__ == '__main__':
app.run(port=8000)
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = 'your_webhook_secret';
// Parse raw body for signature verification
app.use('/webhooks/dropcolis', express.raw({ type: 'application/json' }));
function verifySignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Extract signature value (format: "v1=signature")
const sig = signature.startsWith('v1=') ? signature.slice(3) : signature;
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(sig)
);
}
app.post('/webhooks/dropcolis', (req, res) => {
const signature = req.headers['drop-signature'] || '';
// Verify signature
if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.body.toString());
const { type, data } = event;
switch (type) {
case 'order.accepted':
console.log(`Order ${data.order_id} accepted`);
break;
case 'courier.assigned':
console.log(`Courier ${data.courier?.name} assigned`);
break;
case 'order.delivered':
console.log(`Order ${data.order_id} delivered!`);
// Mark complete, send confirmation
break;
case 'order.failed':
console.log(`Delivery failed: ${data.failure_reason}`);
// Handle failed delivery
break;
}
res.json({ received: true });
});
app.listen(8000, () => console.log('Webhook server running on port 8000'));
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.Map;
@RestController
@RequestMapping("/webhooks")
public class WebhookController {
private static final String WEBHOOK_SECRET = "your_webhook_secret";
@PostMapping("/dropcolis")
public ResponseEntity<Map<String, Boolean>> handleWebhook(
@RequestHeader("Drop-Signature") String signature,
@RequestBody String payload) {
// Verify signature
if (!verifySignature(payload, signature, WEBHOOK_SECRET)) {
return ResponseEntity.status(401).body(Map.of("received", false));
}
// Parse event
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> event = mapper.readValue(payload, Map.class);
String type = (String) event.get("type");
Map<String, Object> data = (Map<String, Object>) event.get("data");
// Handle different event types
switch (type) {
case "order.accepted":
System.out.println("Order " + data.get("order_id") + " accepted");
break;
case "courier.assigned":
Map<String, Object> courier = (Map<String, Object>) data.get("courier");
System.out.println("Courier " + courier.get("name") + " assigned");
break;
case "order.picked_up":
System.out.println("Order " + data.get("order_id") + " picked up");
break;
case "order.out_for_delivery":
System.out.println("Order out for delivery, ETA: " + data.get("eta"));
break;
case "order.delivered":
System.out.println("Order " + data.get("order_id") + " delivered!");
// Mark order as complete
break;
case "order.failed":
System.out.println("Delivery failed: " + data.get("failure_reason"));
// Handle failed delivery
break;
}
return ResponseEntity.ok(Map.of("received", true));
}
private boolean verifySignature(String payload, String signature, String secret) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(
secret.getBytes("UTF-8"), "HmacSHA256"
);
mac.init(secretKey);
byte[] hash = mac.doFinal(payload.getBytes("UTF-8"));
String expected = bytesToHex(hash);
// Extract signature value (format: "v1=signature")
String sig = signature.startsWith("v1=") ? signature.substring(3) : signature;
return MessageDigest.isEqual(expected.getBytes(), sig.getBytes());
} catch (Exception e) {
return false;
}
}
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
<?php
$webhookSecret = 'your_webhook_secret';
// Get raw payload and signature
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_DROP_SIGNATURE'] ?? '';
// Verify signature
function verifySignature($payload, $signature, $secret) {
$expected = hash_hmac('sha256', $payload, $secret);
// Extract signature value (format: "v1=signature")
if (strpos($signature, 'v1=') === 0) {
$signature = substr($signature, 3);
}
return hash_equals($expected, $signature);
}
if (!verifySignature($payload, $signature, $webhookSecret)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Parse event
$event = json_decode($payload, true);
$type = $event['type'];
$data = $event['data'];
switch ($type) {
case 'order.accepted':
error_log("Order {$data['order_id']} accepted");
break;
case 'courier.assigned':
error_log("Courier {$data['courier']['name']} assigned");
break;
case 'order.delivered':
error_log("Order {$data['order_id']} delivered!");
// Mark complete in database
break;
case 'order.failed':
error_log("Delivery failed: {$data['failure_reason']}");
// Handle failed delivery
break;
}
// Return 200 OK
http_response_code(200);
echo json_encode(['received' => true]);
?>
Types d'evenements webhook
| Event Type | Description | Champs de donnees |
|---|---|---|
order.accepted |
Commande acceptee par le systeme | order_id, partner_order_id, status |
courier.assigned |
Coursier assigne a la commande | order_id, courier (name, phone) |
order.picked_up |
Colis ramasse | order_id, picked_up_at |
order.in_transit |
Colis en transit | order_id, eta |
order.out_for_delivery |
En cours de livraison finale | order_id, eta, courier |
order.delivered |
Livre avec succes | order_id, delivered_at, signature_url |
order.failed |
Livraison echouee | order_id, failure_reason, next_attempt |
order.cancelled |
Commande annulee | order_id, cancelled_by, reason |
Signature Verification
Verifiez toujours les signatures webhook pour vous assurer que les evenements proviennent de Dropcolis:
// Webhook headers sent by Dropcolis:
// - Drop-Event-Id: Unique event UUID
// - Drop-Event-Timestamp: Event timestamp
// - Drop-Signature: HMAC-SHA256 signature (v1=hex)
// Signature is calculated as:
signature = HMAC_SHA256(webhook_secret, raw_request_body)
// Compare with Drop-Signature header (after removing "v1=" prefix)
Error Handling
Codes de statut HTTP
| Code | Signification | Action |
|---|---|---|
200 | Succes | Requete terminee |
201 | Cree | Ressource creee (commandes) |
400 | Requete incorrecte | Corrigez le format de la requete |
401 | Non autorise | Verifiez les cles API/signature |
404 | Non trouve | La ressource n'existe pas |
409 | Conflit | ID de commande en double |
422 | Erreur de validation | Corrigez les problemes de validation des donnees |
429 | Limite de debit atteinte | Ralentissez, reessayez plus tard |
500 | Erreur serveur | Reessayez avec delai |
Format de reponse d'erreur
{
"error": {
"code": "validation_error",
"message": "Invalid postal code format",
"details": {
"field": "dropoff.address.postal_code",
"value": "INVALID",
"expected": "Canadian postal code (e.g., H1A1A1)"
}
},
"request_id": "req_abc123def456"
}
Strategie de nouvelle tentative
import time
def make_request_with_retry(func, max_retries=3):
for attempt in range(max_retries):
result, status = func()
if status < 500 and status != 429:
return result, status
# Exponential backoff: 1s, 2s, 4s
wait_time = 2 ** attempt
print(f"Retrying in {wait_time}s...")
time.sleep(wait_time)
return result, status # Return last response
async function makeRequestWithRetry(requestFunc, maxRetries = 3) {
let lastResult, lastStatus;
for (let attempt = 0; attempt < maxRetries; attempt++) {
const { data, status } = await requestFunc();
lastResult = data;
lastStatus = status;
if (status < 500 && status !== 429) {
return { data, status };
}
// Exponential backoff: 1s, 2s, 4s
const waitTime = Math.pow(2, attempt) * 1000;
console.log(`Retrying in ${waitTime / 1000}s...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
return { data: lastResult, status: lastStatus };
}
public <T> ApiResponse<T> makeRequestWithRetry(
Supplier<ApiResponse<T>> requestFunc,
int maxRetries) {
ApiResponse<T> lastResponse = null;
for (int attempt = 0; attempt < maxRetries; attempt++) {
lastResponse = requestFunc.get();
int status = lastResponse.getStatus();
if (status < 500 && status != 429) {
return lastResponse;
}
// Exponential backoff: 1s, 2s, 4s
long waitTime = (long) Math.pow(2, attempt) * 1000;
System.out.println("Retrying in " + (waitTime / 1000) + "s...");
try {
Thread.sleep(waitTime);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
return lastResponse;
}
function makeRequestWithRetry(callable $requestFunc, int $maxRetries = 3): array
{
$lastResult = null;
$lastStatus = null;
for ($attempt = 0; $attempt < $maxRetries; $attempt++) {
$response = $requestFunc();
$lastResult = $response['data'];
$lastStatus = $response['status'];
if ($lastStatus < 500 && $lastStatus !== 429) {
return $response;
}
// Exponential backoff: 1s, 2s, 4s
$waitTime = pow(2, $attempt);
echo "Retrying in {$waitTime}s...\n";
sleep($waitTime);
}
return ['data' => $lastResult, 'status' => $lastStatus];
}
Best Practices
Securite
- Gardez les secrets en securite: N'exposez jamais les cles API dans le code cote client
- Utilisez HTTPS: Utilisez toujours HTTPS en production
- Verifiez les webhooks: Validez toujours les signatures webhook
- Effectuez une rotation des cles: Effectuez periodiquement une rotation des cles API
Fiabilite
- Utilisez des cles d'idempotence: Evitez les commandes en double lors des nouvelles tentatives
- Implementez les nouvelles tentatives: Utilisez un delai exponentiel pour les erreurs transitoires
- Gerez les delais d'attente: Definissez des delais d'attente de requete appropries (30s recommande)
- Journalisez les ID de requete: Stockez request_id pour le debogage
Performance
- Regroupez intelligemment: Evitez de creer trop de commandes simultanement
- Mettez en cache les devis: Les devis sont valides pendant 24 heures
- Utilisez les webhooks: Preferez les webhooks a l'interrogation pour les mises a jour de statut
SDK & Libraries
SDK officiels et communautaires pour les langages populaires:
Ressources
- Swagger UI - Explorateur d'API interactif
- ReDoc - Documentation API alternative
- Reference API - Reference complete des points de terminaison
- Guide de creation de commandes - Guide detaille des commandes
- Guide des devis - Integration des tarifs
- Guide de suivi - Details du suivi des commandes