Integrate with Drogo
Two endpoints, one credential, and your orders are on the road.
Drogo runs the delivery side — drivers, shifts, routes, proof of delivery, the customer's live map. Your system keeps owning the order. This page is everything you build to connect the two; there is nothing to write on our side for you.
GET /drogo/health/
One envelope in both directions, one event per request, JSON over HTTPS. No SDK to install, no library to pin, no polling.
01 — What you build
Two endpoints
These are the defaults. If your namespace looks different, tell us the two paths and we set them on your integration — it is a field, not a release.
Where we deliver everything that happens to the delivery: a driver assigned, a status change, the proof photo, a review. You answer with one of the six answers below.
Cheap, authenticated, no side effects. We probe it every five minutes and show the result next to your integration, so a broken connection is visible before an order is.
Keep the trailing slash if your framework distinguishes events/ from
events — Django does, and a slash-less POST is refused at the front door in a
way that reads like you rejecting us.
02 — The envelope
Every event looks the same
One shape for all thirty-odd types, in both directions. What changes is
type and what sits in data.
{
"event_id": "9f2c1b6a-52d0-4a15-9c4e-1e0f9d2a77b1", // dedupe on this
"type": "order.delivery_status_changed",
"version": 1,
"occurred_at": "2026-09-24T18:04:11.412Z", // when it happened, not when it was sent
"origin": "driver_app", // human | driver_app | partner | automation
"fleet": {"drogo_id": 7},
"tenant": {"drogo_id": 15, "partner_id": "element"}, // your store, on order events
"subject": {"kind": "order", "drogo_id": 9421, "partner_id": "662164"},
"data": {"status": "at_customer", "driver": {"partner_id": "58"}}
}
subject is what the event is about, carrying both ids once we know both — your
id is the one you sent us. Inside data, absent is not null:
an absent key means this event says nothing about that field, and null means
the field was explicitly cleared.
Send snake_case. We accept camelCase too, but snake_case is the canonical
spelling and the one every example here uses.
03 — Your answer
Six answers, and why they differ
The distinctions are the whole point: they separate our retry logic from your business rules. "I deliberately ignored this" and "I could not do this" are different facts, and a wire that collapses them turns a normal race into a page at 2am.
5xx — We back off and retry, then dead-letter. Anything you can requeue belongs here.
One answer carries more than a verdict: order.created comes back with
data.tracking_number and data.tracking_url — the live map your
customer opens. A duplicate answer carries the same data and the same ids,
because a retry has usually lost the first answer.
04 — Signature
Prove the request is ours
Credentials say who knocked. A signature says the body was not changed on the way. Every request we send you carries one, computed with a secret we mint for your integration and show once.
X-Drogo-Signature: t=1758700000,v1=6f1c… // hmac-sha256("1758700000." + raw_body)
import hmac, hashlib, time
def verify(header: str, raw_body: bytes, secret: str, skew: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t, v1 = parts["t"], parts["v1"]
if abs(time.time() - int(t)) > skew: # a replayed request
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
const crypto = require("crypto");
function verify(header, rawBody, secret, skew = 300) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > skew) return false; // a replayed request
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
Verify the raw body
Not a re-serialized parse of it. Re-encoding changes whitespace and key order, and a correct request then reads as a forgery.
Dedupe on event_id, not on the signature
A retry of the same event carries a fresh timestamp and therefore a fresh signature
over the same event_id. The timestamp is inside what is signed — that is
what makes the five-minute window worth checking.
You may sign your requests to us with the same secret and the same rule. Once you do, ask
us to require it: after that an unsigned, altered or stale request is answered
401 invalid_signature.
05 — Sending orders
One call puts an order on a driver
curl https://api.drogo.app/api/v3/events \
-u "$DROGO_CLIENT_ID:$DROGO_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"event_id": "1f0a…", "type": "order.created", "version": 1,
"occurred_at": "2026-09-24T18:00:00Z", "origin": "human",
"subject": {"kind": "order", "partner_id": "662164"},
"data": {
"payment_type": "CASH", "grand_total": 42.50, "tax_total": 2.50, "sub_total": 40.00,
"shipping_address": {"address": "1 Main St", "latitude": 34.05, "longitude": -118.24},
"customer": {"external_id": "c-88", "first_name": "Ann", "phone": "+13105550101"},
"items": [{"product_name": "Item", "price": 40.00, "quantity": 1}]
}
}'
→ 200 {"applied": true,
"subject": {"drogo_id": 84213, "partner_id": "662164"},
"data": {"tracking_number": "kK3f…", "tracking_url": "https://api.drogo.app/track/kK3f…"}}
The credential identifies your integration and the fleet it belongs to — you never state who you are in the payload, and you cannot post into anyone else's orders by naming them.
06 — Four rules
What a receiver must implement
Everything else is optional. These four are what make a delivery lane survive a retry, a reconnect and a race between two events about the same order.
Idempotency
The same event_id twice applies once. Remember them for 30 days.
Staleness
Keep the last applied occurred_at per subject. An older event is ignored
with "reason": "stale" — not an error. This is what makes a late
at_customer harmless after delivered.
Assert only what you know
Omit a field you have no information about. Creating an order says nothing about its driver; it does not claim the order is unassigned.
No echo
An event you applied from us does not come back to us as your own event. We do the
same for you — echo suppression reads origin, not a header.
07 — What travels
The catalogue
Order events — carry your store as tenant
Fleet events — about a driver, not a store
Delivery status is not your kitchen status
order.delivery_status_changed carries the delivery state machine only.
Preparation, packing and whatever else happens in your building stays yours and never
travels under these names.
A route change is one event
driver.route_changed carries the driver's whole queue in order, never one
event per stop — splitting it would make a half-applied route possible.
Where an integration exists, every fact about the delivery is sent — the photo, the
signature, both reviews, the rating — whether or not you have somewhere to put it yet. You
can answer not_applicable and drop it; that stays your decision to change
later.
08 — Going live
Six steps, in this order
-
1
We create your integration
us · a row, no deploy
Your base url and your two paths, plus credentials for calling us.
-
2
Health check goes green
you · one endpoint
We probe it from our side and watch it for five minutes.
-
3
We hand over your signing secret
us · shown once
You verify a real signed event with it.
-
4
You send a test order
you · one POST
You get the tracking number back and store it against your order.
-
5
We run one delivery end to end
both · a real driver on a test order
Assignment, statuses, proof, review — everything your side will see in production.
-
6
We turn on signature enforcement
us · one switch
From then on an unsigned or stale request to us is refused.
Start
Tell us where your orders live
Send us your base url and the two paths you can expose. Steps 1 and 3 take about ten minutes on our side, and you can be posting test orders the same day.