Skip to main content

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:

  1. Get a rate quote (display to user, store the total)
  2. Create the shipment with payment_source: "wallet"
  3. Store id and merchant_reference for correlation
  4. Receive webhook updates as the package moves
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;
}

Webhook handler

Verify the signature, look up your order by merchant_reference (or by stored shipment_id), and update its status.

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);
},
);

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 fieldWhere to storeWhy
iddodo_shipment_id column on your orderLook up details, cancel
tracking_codedodo_tracking_code columnBuild tracking URLs in your UI/emails
shipment_numberoptionalHuman display

Always keep merchant_reference set to your own order ID — it's the cleanest way to correlate webhooks back to your records.