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

  1. Get API Keys: Log in to your Partner Dashboard and create API keys
  2. Install Dependencies: Add HMAC library for signature generation
  3. Make Your First Call: Test with a rate quote request
  4. Create Orders: Start creating delivery orders

Test Credentials

Use these sandbox credentials for development:

Sandbox API Keys:
  • 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:

Signature Algorithm
// 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)
Important: The request body must be the exact JSON string used in the signature calculation. Use consistent JSON serialization (no extra whitespace, same key order).

Authentication Helper Classes

dropcolis_client.py
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")
dropcolisClient.js
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;
DropcolisClient.php
<?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";
?>
DropcolisClient.java
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

POST /v1/rates/quote

Get delivery rate quote before creating an order

POST /v1/orders

Create a new delivery order

GET /v1/orders/{order_id}

Retrieve order details and current status

GET /v1/orders

List all orders with optional filters

POST /v1/orders/{order_id}/cancel

Cancel a pending order

POST /v1/webhooks/test

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.

Get Rate Quote
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']}")
Get Rate Quote
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}`);
}
Get Rate Quote
// 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"));
Get Rate Quote
// 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
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

Rate Quote 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.

Idempotency: Always include an Idempotency-Key header to prevent duplicate orders. If you retry a request with the same key, you will get the original response.

Required Fields

FieldTypeDescription
partner_order_idstringYour unique order identifier
service_levelstringstandard, express, or same_day
pickupobjectPickup location with name, phone, email, address, time_window
dropoffobjectDelivery location with same structure as pickup
parcelsarray1-50 parcels with dimensions, weight, and value

Complete Order Example

Create Order - Python
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')}")
Create Order - JavaScript
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}`);
}
Create Order - Java
// 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"));
Create Order - PHP
// 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

Create Order Response (201 Created)
{
    "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.

Get Order Status
# 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']}")
Get Order 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}`);
    });
}
Get Order 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"));
    }
}
Get Order 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

StatusDescription
pendingOrder received, awaiting processing
acceptedOrder accepted by Dropcolis
courier_assignedCourier has been assigned
picked_upPackage picked up from origin
in_transitPackage in transit
out_for_deliveryOut for final delivery
deliveredSuccessfully delivered
failedDelivery attempt failed
cancelledOrder was cancelled

Public Tracking Page

Share the tracking_url with your customers. They can track their delivery without authentication:

Tracking URL
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.

Cancel Order
# 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']}")
Cancel Order
// 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}`);
}
Cancel Order
// 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());
}
Cancel Order
// 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";
}
Cancellation Policy: Orders can only be cancelled while in 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

  1. Create an HTTPS endpoint on your server to receive webhook events
  2. Configure your webhook URL in the Partner Dashboard
  3. Verify webhook signatures to ensure authenticity
  4. Respond with 200 OK to acknowledge receipt

Example Webhook Handler

webhook_handler.py
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)
webhookHandler.js
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'));
WebhookController.java
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();
    }
}
webhook.php
<?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 TypeDescriptionData 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:

Signature Verification
// 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)
Security Warning: Never skip signature verification in production! Attackers could send fake events to your webhook endpoint.

Error Handling

HTTP Status Codes

CodeMeaningAction
200SuccessRequest completed
201CreatedResource created (orders)
400Bad RequestFix request format
401UnauthorizedCheck API keys/signature
404Not FoundResource does not exist
409ConflictDuplicate order ID
422Validation ErrorFix data validation issues
429Rate LimitedSlow down, retry later
500Server ErrorRetry with backoff

Error Response Format

Error Response
{
    "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

Exponential Backoff - Python
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
Exponential Backoff - JavaScript
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 };
}
Exponential Backoff - Java
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;
}
Exponential Backoff - PHP
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:

Coming Soon: Official SDKs for Python, JavaScript, PHP, and Ruby are in development. In the meantime, use the helper classes provided in this guide.

Resources

Open Swagger UI Partner Dashboard