Drogo Integration v3 Product Request access

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.

Your system POS · storefront · dispatch POST /drogo/events/
GET  /drogo/health/
Drogo api.drogo.app POST /api/v3/events

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.

POST {your_base_url}/drogo/events/

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.

GET {your_base_url}/drogo/health/

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.

200 {"applied": true} Done.
404 {"error": "unknown_subject"} You have never heard of this order. We retry only after creating it.
409 {"error": "conflict", "detail": "…"} You know it and refuse the transition. A person decides what happens next.
422 {"error": "invalid" | "unsupported_version", "detail": "…"} We are wrong. We will not retry unchanged — the detail is what our engineer reads.
429
5xx
— We back off and retry, then dead-letter. Anything you can requeue belongs here.
applied ignored on purpose never retried retried with backoff

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)

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.

01

Idempotency

The same event_id twice applies once. Remember them for 30 days.

02

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.

03

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.

04

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

order.created order.content_changed order.driver_assigned order.driver_unassigned order.delivery_status_changed order.proof_captured order.issue_reported order.review_created

Fleet events — about a driver, not a store

driver.created driver.profile_changed driver.deactivated driver.shift_changed driver.location_changed driver.rating_changed driver.salary_changed driver.route_changed

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. 1

    We create your integration

    us · a row, no deploy

    Your base url and your two paths, plus credentials for calling us.

  2. 2

    Health check goes green

    you · one endpoint

    We probe it from our side and watch it for five minutes.

  3. 3

    We hand over your signing secret

    us · shown once

    You verify a real signed event with it.

  4. 4

    You send a test order

    you · one POST

    You get the tracking number back and store it against your order.

  5. 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. 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.