Complete Integration Guide
This guide covers everything you need to integrate Dropcolis delivery services into your application. From authentication to webhooks, you will find detailed examples in multiple programming languages.
Quick Start
Get started in 5 minutes
- Get API Keys: Log in to your Partner Dashboard and create API keys
- Install Dependencies: Add HMAC library for signature generation
- Make Your First Call: Test with a rate quote request
- Create Orders: Start creating delivery orders
Test Credentials
Use these sandbox credentials for development:
- Public Key:
pk_test_dropcolis_partner - Secret Key:
sk_test_secret_dropcolis_partner - Base URL:
http://localhost:5001/v1
Authentication
All API requests require HMAC-SHA256 authentication. You must include three headers with every request:
| Header | Description | Example |
|---|---|---|
X-Api-Key |
Your public API key | pk_test_dropcolis_partner |
X-Timestamp |
Unix timestamp (seconds since epoch) | 1702051200 |
X-Signature |
HMAC-SHA256 signature with v1= prefix | v1=abc123... |
Signature Generation
The signature protects your requests from tampering. Here is how to generate it:
// 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)
Authentication Helper Classes
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());
}
}
API Overview
The Dropcolis API follows RESTful conventions. All endpoints are prefixed with /v1.
Available Endpoints
Get delivery rate quote before creating an order
Create a new delivery order
Retrieve order details and current status
List all orders with optional filters
Cancel a pending order
Send a test webhook to your endpoint
Get Rate Quotes
Before creating an order, you can get a rate quote to show pricing to your customers.
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"
}
Create Orders
Create delivery orders with pickup and dropoff details, parcel information, and optional preferences.
Required Fields
| Field | Type | Description |
|---|---|---|
partner_order_id | string | Your unique order identifier |
service_level | string | standard, express, or same_day |
pickup | object | Pickup location with name, phone, email, address, time_window |
dropoff | object | Delivery location with same structure as pickup |
parcels | array | 1-50 parcels with dimensions, weight, and value |
Complete Order Example
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"
}
Track Orders
Retrieve the current status and full history of any order.
# 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";
}
}
Order Statuses
| Status | Description |
|---|---|
pending | Order received, awaiting processing |
accepted | Order accepted by Dropcolis |
courier_assigned | Courier has been assigned |
picked_up | Package picked up from origin |
in_transit | Package in transit |
out_for_delivery | Out for final delivery |
delivered | Successfully delivered |
failed | Delivery attempt failed |
cancelled | Order was cancelled |
Public Tracking Page
Share the tracking_url with your customers. They can track their delivery without authentication:
http://localhost:5001/track/dc_9f7a1f2e3d4c5b6a
Cancel Orders
Cancel an order before it is picked up. Orders that are already in transit cannot be cancelled.
# 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.
Webhook Setup
Webhooks notify your system in real-time when order status changes. Instead of polling the API, you receive instant notifications.
Setting Up Your Webhook Endpoint
- Create an HTTPS endpoint on your server to receive webhook events
- Configure your webhook URL in the Partner Dashboard
- Verify webhook signatures to ensure authenticity
- Respond with 200 OK to acknowledge receipt
Example Webhook Handler
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]);
?>
Webhook Event Types
| Event Type | Description | Data Fields |
|---|---|---|
order.accepted |
Order accepted by system | order_id, partner_order_id, status |
courier.assigned |
Courier assigned to order | order_id, courier (name, phone) |
order.picked_up |
Package picked up | order_id, picked_up_at |
order.in_transit |
Package in transit | order_id, eta |
order.out_for_delivery |
Out for final delivery | order_id, eta, courier |
order.delivered |
Successfully delivered | order_id, delivered_at, signature_url |
order.failed |
Delivery failed | order_id, failure_reason, next_attempt |
order.cancelled |
Order cancelled | order_id, cancelled_by, reason |
Signature Verification
Always verify webhook signatures to ensure events are from 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
HTTP Status Codes
| Code | Meaning | Action |
|---|---|---|
200 | Success | Request completed |
201 | Created | Resource created (orders) |
400 | Bad Request | Fix request format |
401 | Unauthorized | Check API keys/signature |
404 | Not Found | Resource does not exist |
409 | Conflict | Duplicate order ID |
422 | Validation Error | Fix data validation issues |
429 | Rate Limited | Slow down, retry later |
500 | Server Error | Retry with backoff |
Error Response Format
{
"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"
}
Retry Strategy
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
Security
- Keep secrets secure: Never expose API keys in client-side code
- Use HTTPS: Always use HTTPS in production
- Verify webhooks: Always validate webhook signatures
- Rotate keys: Periodically rotate API keys
Reliability
- Use idempotency keys: Prevent duplicate orders on retries
- Implement retries: Use exponential backoff for transient errors
- Handle timeouts: Set appropriate request timeouts (30s recommended)
- Log request IDs: Store request_id for debugging
Performance
- Batch wisely: Avoid creating too many orders simultaneously
- Cache rate quotes: Quotes are valid for 24 hours
- Use webhooks: Prefer webhooks over polling for status updates
SDK & Libraries
Official and community SDKs for popular languages:
Resources
- Swagger UI - Interactive API explorer
- ReDoc - Alternative API documentation
- API Reference - Complete endpoint reference
- Order Creation Guide - Detailed order guide
- Rate Quotes Guide - Pricing integration
- Tracking Guide - Order tracking details