Code Examples
End-to-end snippets for the most common integration flow: get a quote, create a shipment, receive webhooks.
Full integration
The pattern is always the same:
- Get a rate quote (display to user, store the total)
- Create the shipment with
payment_source: "wallet" - Store
idandmerchant_referencefor correlation - Receive webhook updates as the package moves
- Node.js
- Python
- PHP
const axios = require('axios');
const dodo = axios.create({
baseURL: 'https://api.dodo.co.tz/api/v1',
headers: { 'X-API-Key': process.env.DODO_API_KEY },
});
async function bookDelivery(myOrder) {
// 1. Quote
const { data: quote } = await dodo.post('/merchant/shipments/rates', {
pickup_latitude: myOrder.pickup.lat,
pickup_longitude: myOrder.pickup.lng,
dropoff_latitude: myOrder.dropoff.lat,
dropoff_longitude: myOrder.dropoff.lng,
items: [{
description: myOrder.description,
weight_kg: myOrder.weight_kg,
quantity: 1,
}],
});
console.log(`Delivery cost: ${quote.total_amount} TZS via ${quote.vehicle_name}`);
// 2. Create
const { data: shipment } = await dodo.post('/merchant/shipments', {
sender_name: 'My Store',
sender_phone: '+255712345678',
pickup_address: myOrder.pickup.address,
pickup_latitude: myOrder.pickup.lat,
pickup_longitude: myOrder.pickup.lng,
receiver_name: myOrder.customer.name,
receiver_phone: myOrder.customer.phone,
dropoff_address: myOrder.dropoff.address,
dropoff_latitude: myOrder.dropoff.lat,
dropoff_longitude: myOrder.dropoff.lng,
items: [{
description: myOrder.description,
weight_kg: myOrder.weight_kg,
quantity: 1,
}],
payment_source: 'wallet',
merchant_reference: myOrder.id, // ← your order ID
});
// 3. Store the Dodo IDs against your order
await db.orders.update({
where: { id: myOrder.id },
data: {
dodo_shipment_id: shipment.id,
dodo_tracking_code: shipment.tracking_code,
},
});
return shipment;
}
import os
import requests
BASE = 'https://api.dodo.co.tz/api/v1'
HEADERS = {'X-API-Key': os.environ['DODO_API_KEY']}
def book_delivery(my_order):
# 1. Quote
quote = requests.post(
f'{BASE}/merchant/shipments/rates',
headers=HEADERS,
json={
'pickup_latitude': my_order['pickup']['lat'],
'pickup_longitude': my_order['pickup']['lng'],
'dropoff_latitude': my_order['dropoff']['lat'],
'dropoff_longitude': my_order['dropoff']['lng'],
'items': [{
'description': my_order['description'],
'weight_kg': my_order['weight_kg'],
'quantity': 1,
}],
},
).json()
print(f"Delivery cost: {quote['total_amount']} TZS via {quote['vehicle_name']}")
# 2. Create
shipment = requests.post(
f'{BASE}/merchant/shipments',
headers=HEADERS,
json={
'sender_name': 'My Store',
'sender_phone': '+255712345678',
'pickup_address': my_order['pickup']['address'],
'pickup_latitude': my_order['pickup']['lat'],
'pickup_longitude': my_order['pickup']['lng'],
'receiver_name': my_order['customer']['name'],
'receiver_phone': my_order['customer']['phone'],
'dropoff_address': my_order['dropoff']['address'],
'dropoff_latitude': my_order['dropoff']['lat'],
'dropoff_longitude': my_order['dropoff']['lng'],
'items': [{
'description': my_order['description'],
'weight_kg': my_order['weight_kg'],
'quantity': 1,
}],
'payment_source': 'wallet',
'merchant_reference': my_order['id'],
},
).json()
# 3. Store IDs against your order
db.orders.update(
my_order['id'],
dodo_shipment_id=shipment['id'],
dodo_tracking_code=shipment['tracking_code'],
)
return shipment
<?php
$base = 'https://api.dodo.co.tz/api/v1';
$key = getenv('DODO_API_KEY');
function dodo($method, $path, $body = null) {
global $base, $key;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $base . $path,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_POSTFIELDS => $body ? json_encode($body) : null,
CURLOPT_HTTPHEADER => [
"X-API-Key: $key",
'Content-Type: application/json',
],
]);
return json_decode(curl_exec($ch), true);
}
// 1. Quote
$quote = dodo('POST', '/merchant/shipments/rates', [
'pickup_latitude' => $myOrder['pickup']['lat'],
'pickup_longitude' => $myOrder['pickup']['lng'],
'dropoff_latitude' => $myOrder['dropoff']['lat'],
'dropoff_longitude' => $myOrder['dropoff']['lng'],
'items' => [[
'description' => $myOrder['description'],
'weight_kg' => $myOrder['weight_kg'],
'quantity' => 1,
]],
]);
// 2. Create
$shipment = dodo('POST', '/merchant/shipments', [
'sender_name' => 'My Store',
'sender_phone' => '+255712345678',
'pickup_address' => $myOrder['pickup']['address'],
'pickup_latitude' => $myOrder['pickup']['lat'],
'pickup_longitude' => $myOrder['pickup']['lng'],
'receiver_name' => $myOrder['customer']['name'],
'receiver_phone' => $myOrder['customer']['phone'],
'dropoff_address' => $myOrder['dropoff']['address'],
'dropoff_latitude' => $myOrder['dropoff']['lat'],
'dropoff_longitude' => $myOrder['dropoff']['lng'],
'items' => [[
'description' => $myOrder['description'],
'weight_kg' => $myOrder['weight_kg'],
'quantity' => 1,
]],
'payment_source' => 'wallet',
'merchant_reference' => $myOrder['id'],
]);
// 3. Persist
db_update_order($myOrder['id'], [
'dodo_shipment_id' => $shipment['id'],
'dodo_tracking_code' => $shipment['tracking_code'],
]);
Webhook handler
Verify the signature, look up your order by merchant_reference (or by stored shipment_id), and update its status.
- Node.js
- Python
const crypto = require('crypto');
const express = require('express');
const app = express();
const SECRET = process.env.DODO_WEBHOOK_SECRET;
function verify(rawBody, timestamp, signature) {
const message = Buffer.concat([
Buffer.from(`${timestamp}.`),
Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody),
]);
const expected = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(message)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
app.post(
'/webhooks/dodo',
express.raw({ type: 'application/json' }),
async (req, res) => {
const sig = req.headers['x-dodo-signature'];
const ts = req.headers['x-dodo-timestamp'];
const event = req.headers['x-dodo-event'];
if (!sig || !ts || !verify(req.body, ts, sig)) {
return res.status(401).send('Invalid signature');
}
const data = JSON.parse(req.body);
// Idempotency: skip duplicates
const inserted = await db.query(
`INSERT INTO dodo_webhook_events (shipment_id, event)
VALUES ($1, $2) ON CONFLICT DO NOTHING RETURNING id`,
[data.shipment_id, event],
);
if (inserted.rowCount === 0) return res.sendStatus(200);
// Look up by your merchant_reference if you stored it
const order = await db.orders.findOne({
where: { id: data.merchant_reference },
});
if (!order) return res.sendStatus(200); // not ours, ignore
switch (event) {
case 'delivery.picked_up':
await db.orders.update(order.id, { status: 'in_delivery' });
break;
case 'delivery.delivered':
await db.orders.update(order.id, {
status: 'delivered',
delivered_at: data.delivered_at,
});
notifyCustomer(order, 'Your order has arrived');
break;
case 'delivery.failed':
await db.orders.update(order.id, {
status: 'delivery_failed',
failure_reason: data.failure_reason,
});
notifyOps(`Delivery failed for order ${order.id}: ${data.failure_reason}`);
break;
case 'shipment.cancelled':
await db.orders.update(order.id, { status: 'cancelled' });
break;
}
res.sendStatus(200);
},
);
import hmac, hashlib, os
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ['DODO_WEBHOOK_SECRET'].encode()
def verify(raw_body, timestamp, signature):
message = f"{timestamp}.".encode() + raw_body
expected = 'sha256=' + hmac.new(SECRET, message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route('/webhooks/dodo', methods=['POST'])
def webhook():
sig = request.headers.get('X-Dodo-Signature', '')
ts = request.headers.get('X-Dodo-Timestamp', '')
event = request.headers.get('X-Dodo-Event', '')
raw = request.get_data()
if not sig or not ts or not verify(raw, ts, sig):
return 'Unauthorized', 401
data = request.json
# Idempotency
inserted = db.execute(
"INSERT INTO dodo_webhook_events (shipment_id, event) "
"VALUES (%s, %s) ON CONFLICT DO NOTHING",
(data['shipment_id'], event),
)
if inserted.rowcount == 0:
return '', 200
order = db.orders.find_one(id=data.get('merchant_reference'))
if not order:
return '', 200
if event == 'delivery.delivered':
order.update(status='delivered', delivered_at=data['delivered_at'])
notify_customer(order, 'Your order has arrived')
elif event == 'delivery.failed':
order.update(status='delivery_failed', failure_reason=data['failure_reason'])
notify_ops(f"Delivery failed for order {order.id}: {data['failure_reason']}")
elif event == 'shipment.cancelled':
order.update(status='cancelled')
return '', 200
Polling fallback
If you can't (or don't want to) host a webhook endpoint, poll GET /merchant/shipments/{id} periodically:
async function pollUntilTerminal(shipmentId) {
const terminal = ['delivered', 'cancelled', 'failed'];
while (true) {
const { data } = await dodo.get(`/merchant/shipments/${shipmentId}`);
if (terminal.includes(data.status)) return data;
await new Promise(r => setTimeout(r, 30_000)); // every 30s
}
}
Webhooks are strongly preferred — they're real-time and don't waste API quota. Polling is a fallback.
What to store on your side
| Dodo field | Where to store | Why |
|---|---|---|
id | dodo_shipment_id column on your order | Look up details, cancel |
tracking_code | dodo_tracking_code column | Build tracking URLs in your UI/emails |
shipment_number | optional | Human display |
Always keep merchant_reference set to your own order ID — it's the cleanest way to correlate webhooks back to your records.