Zoony DevelopersDelivery and maps APIs for Iraqzoonyiraq.com

Send orders, follow them live, find Iraqi addresses

Zoony runs delivery fleets in Iraq. Your system sends an order; a nearby driver gets it in the Zoony Driver app; you get every step by webhook, a tracking link for your customer, and cash on delivery reconciled. The same keys can use Zoony’s Iraqi maps: places and landmarks, drive times with live traffic, and routes.

Order API

Create, follow, mark ready and cancel orders. The same order sent twice is created once.

Webhooks

Every status change, signed, retried for six hours.

Test keys

A pretend driver delivers your test orders step by step. Nothing real moves.

Maps API

Iraqi places and landmarks, drive times with traffic, routes, time/distance tables.

Quick start

  1. The delivery company (or Zoony) creates a test key for you in the dispatch dashboard: Settings → API keys → New key → Test key. Test keys start with zk_test_.
  2. Tell Zoony where to send test webhooks (or set it yourself, below).
  3. Send an order:
curl https://www.zoonyiraq.com/v1/dispatch/orders \
  -H "Authorization: Bearer zk_test_…" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "ORDER-10045",
    "customer": { "name": "Ali Hassan", "phone": "07701234567" },
    "dropoff":  { "lat": 33.3128, "lon": 44.3615, "address": "Karrada, near Al-Wathiq Square" },
    "pickup":   { "name": "Burger House", "phone": "07801234567", "lat": 33.3005, "lon": 44.3893 },
    "prep_min": 12,
    "promise_min": 45,
    "cod_iqd": 18000
  }'

The answer is the order, with its id, status and tracking_url. With a test key, the pretend driver takes it, waits for the food, picks it up and delivers it, a step about every 8 seconds; you get each step by webhook and can read it any time with GET /v1/dispatch/orders/ext:ORDER-10045.

Keys and test mode

Send the key on every request as Authorization: Bearer <key>. Keys are made and revoked by the delivery company’s owner in the dispatch dashboard and are shown once; keep them on your server, never in an app or web page.

KeyStarts withOrders go to
Livezk_The company’s real drivers.
Testzk_test_A test copy of the company with a pretend driver. Test orders are deleted after 7 days and never appear in the company’s board, money or reports.
Merchant keyeitherOne shop’s orders only: pickups default to the shop, and the key sees only that shop’s orders.

Steering test orders

Put in noteWhat the pretend driver does
[fail]Reaches the door and fails (“the customer didn’t answer”).
[no_driver]Never takes it, so you can test your “no driver” handling.

Test webhook address

PUT /v1/dispatch/webhook        (test keys only)
{ "url": "https://your-server.example/zoony-hooks" }

→ { "url": "https://…", "secret": "whsec_…" }

Live webhooks are set by the company in the dashboard.

Limits

Each key may make a set number of requests a minute (600 unless agreed otherwise, up to 6,000; more on request). Every answer carries X-RateLimit-Limit and X-RateLimit-Remaining. Over the limit you get 429 with Retry-After (seconds): wait that long and try again. Webhooks don’t count. The dashboard shows each key’s calls today, over 30 days, and how many were refused.

Orders

PathDoes
POST/v1/dispatch/ordersCreate. 201 with the order; 200 with the existing one when the external_id was sent before.
GET/v1/dispatch/orders/{id}One order. {id} is Zoony’s number or ext:<your external_id>.
POST/v1/dispatch/orders/{id}/readyThe food is ready now (sooner than prep_min said).
POST/v1/dispatch/orders/{id}/cancel{"reason"}. 409 once it’s delivered or failed.

Creating an order

FieldMeaning
external_idYour order number. Strongly recommended: sending it again (a retry) returns the first order instead of a second delivery.
customer.name, customer.phoneWho receives it. Name required.
dropoff.lat, dropoff.lon, dropoff.addressThe door. Coordinates required; the address is what the driver reads. Iraqi addresses are landmarks: “behind the Rafidain bank, 3rd house” helps.
pickup{name, phone, lat, lon} where the driver collects it. A merchant key fills in the shop.
prep_min or ready_atWhen it will be ready. The driver is sent so they arrive about then, not to wait 20 minutes at the counter.
promise_min or window_toThe latest it should arrive (minutes from now, or a time). The order answers promised_by and late; the company sees on-time rates and late orders.
window_fromNot before this time (scheduled deliveries).
cod_iqdCash to collect, in dinars. 0 for paid orders.
delivery_fee_iqdA fee agreed for this order; otherwise the company’s price list applies.
priorityHigher goes first.
require_codeThe customer gives the driver a 4-digit code (in handover_code) to receive it.
noteFor the driver.
parcel_codeThe code on the parcel’s label. When the company requires scans, the driver scans it at pickup and at handover (orders without one get a ZN… code to print).
weight_kg, sizeHow heavy and how big (small, medium, large, xl), so it goes to a vehicle that can carry it.
required_tagsDriver skills the order needs, as a list of the company’s tags (e.g. ["fridge", "fragile"]). Only drivers with all of them get it automatically; a tag the company hasn’t defined is refused with 400 and code unknown_tag.

The order you get back

{
  "id": 81234, "external_id": "ORDER-10045", "status": "on_way",
  "customer": "Ali Hassan", "address": "Karrada, near Al-Wathiq Square", "lat": 33.3128, "lon": 44.3615,
  "driver": { "name": "Mustafa", "phone": "+9647…", "lat": 33.305, "lon": 44.37, "seen_at": "…" },
  "ready_at": "…", "promised_by": "2026-11-05T14:45:00Z", "late": false, "eta": "2026-11-05T14:31:00Z",
  "tracking_url": "https://www.zoonyiraq.com/t/…",
  "cod_iqd": 18000, "cod_collected_iqd": null, "delivery_fee_iqd": null,
  "created_at": "…", "accepted_at": "…", "at_pickup_at": "…", "picked_up_at": "…", "on_way_at": "…",
  "arrived_at": null, "done_at": null, "has_photo": false, "has_signature": false,
  "parcel_code": "ZN7K2MQ4XR", "weight_kg": 2, "size": "small", "return_status": "", "rating": null, "required_tags": []
}

The driver’s position is shown only while the order is under way.

On the tracking link the customer can leave instructions for the driver (customer_note) and move the delivery to a later 2-hour slot, up to twice (window_from/window_to, reschedules); a driver who hadn’t reached the shop is then taken off it and it goes back to new.

Statuses

statusMeansWebhook
newWaiting for a driver (being offered).order.created, order.no_driver if nobody takes it
assignedGiven to a driver who hasn’t confirmed.order.assigned
acceptedThe driver took it; heading to the pickup.order.accepted, then order.at_pickup, order.ready, order.picked_up
on_wayDriving to the customer.order.on_way
arrivedAt the door.order.arrived
deliveredDone; cod_collected_iqd is the cash taken.order.delivered
failedCouldn’t deliver; driver_note says why. If the driver had collected it, it goes back to the shop: return_status is returning, then returned (returned_at). The company may also send it out again.order.failed, then order.returning and order.returned, or order.retry
cancelledCancelled by you or the company.order.cancelled
(any open status)The customer moved the delivery time.order.rescheduled

Webhooks

Each event is a POST of {"id", "event", "at", "order"}, with the order as above. Answer 2xx within 10 seconds. Failed posts are tried again after 30 s, 2 min, 10 min, 30 min, 2 h and 6 h. Events can arrive more than once and, rarely, out of order: use id to skip repeats and the order’s status as the truth.

Checking the signature

X-Zoony-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" with your whsec_… secret. Reject bodies whose signature doesn’t match or whose t is more than 5 minutes old.

// Node.js
const crypto = require('crypto');
function verify(rawBody, header, secret) {
  const [t, v1] = header.split(',').map((p) => p.split('=')[1]);
  const mac = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  return fresh && crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(v1));
}

Order intake (no code on your side)

A delivery app or restaurant system that already sends orders as JSON can send them to Zoony unchanged. The company makes an intake in the dashboard (Settings → Order intake), which gives a secret address, and says where each field is in your JSON. Every post to the address becomes an order.

POST https://www.zoonyiraq.com/v1/intake/zi_…        (no API key: the address is the secret)
Content-Type: application/json

{ "order": { "id": "T-5521", "total": 18000, "notes": "ring twice",
    "customer": { "name": "Ali Hassan", "phone": "07701234567" },
    "address":  { "text": "Karrada", "lat": 33.3128, "lng": 44.3615 },
    "items": [ { "name": "Burger", "quantity": 2 }, { "name": "Cola" } ] } }

→ 201 { "id": 9312, "track_url": "https://www.zoonyiraq.com/t/…" }
MappingExample pathNotes
customer, phone, addressorder.customer.nameCustomer and a place are required.
lat, lonorder.address.latNumbers or numeric text; one "lat,lon" field works too.
cod_iqd, note, reforder.totalThe same ref twice on one intake is created once (200, "duplicate": true).
itemsorder.itemsNames and quantities go into the driver’s note.
prep_min, ready_at, window_from, window_toorder.ready_atRFC 3339, 2026-11-05 14:30 (Baghdad), or unix seconds.
merchant_id, branch_idstore.zoony_idZoony’s shop and branch numbers, unless the intake is fixed to one shop.

Paths use dots between keys, [0] for one list item and [] for all; a | b takes the first that has a value and a + b joins them (customer.first + customer.last). The dashboard has a tester: paste a sample and see the order it would make, without making it.

Signature (optional). When the intake has a header name and secret, each post must carry the HMAC-SHA256 of the raw body in that header, as hex or base64, with or without sha256=. Posts without it get 401.

Refusals are {"error", "code"}: bad_json, mapping (a required field wasn’t found), bad_signature, intake_off, too_big (over 256 KB), rate_limited (over 300 a minute, with Retry-After), or the company’s plan limits. The dashboard keeps the last 200 posts to each intake with their answer and the first 2 KB of the body, to find what went wrong.

Maps API

Keys with Maps API turned on can use Zoony’s Iraqi maps, within a monthly quota agreed with Zoony. Answers carry X-Maps-Quota-Remaining; past the quota you get 429 until the month ends (Baghdad time).

PathDoes
GET/v1/dispatch/maps/search?q=&lat=&lon=&lang=ar|ckb|enPlaces, landmarks, streets and areas, in Arabic, Kurdish or English, nearest first when lat/lon are given.
GET/v1/dispatch/maps/eta?from=lat,lon&to=lat,lon{"seconds", "usual_seconds"}: the drive now with live traffic, and usually.
POST/v1/dispatch/maps/route{"from":{lat,lon}, "to":{lat,lon}, "via":[…], "mode":"car|truck|walk"} → distance, duration, shape (polyline6), turn-by-turn, traffic.
POST/v1/dispatch/maps/matrix{"points":[{lat,lon}…]} (2–25) → {"seconds":[[…]], "km":[[…]]}, row = from, column = to.

Errors

Errors are JSON: {"error": "what went wrong, in plain words"}.

CodeMeans
400Something in the request is missing or wrong; the message says what.
401No key, a wrong key or a revoked one.
403The key can’t do this (e.g. maps not on for it).
404No such order for this key.
409The order is already finished.
429Too many requests (see Retry-After) or the maps quota is used up.
5xxOur side. Retry with backoff; with external_id a retried create never makes a second order.

Going live

  1. Run your flow with a test key: created → delivered, a failed order ([fail]), a cancellation, a retried create, your webhook signature check.
  2. Agree your request limit and, for maps, your monthly quota.
  3. The company creates your live key (zk_…) and sets your live webhook address.
  4. Swap the key. Nothing else changes.
A staging copy of the whole service, with its own data, is available for larger integrations; ask Zoony for its address and keys.

Uptime and support

The service is checked from outside every 5 minutes, the team is alerted when anything is down, and the database is backed up every night with a copy kept off the server. GET https://www.zoonyiraq.com/readyz answers {"ok": true} when everything the API needs is working. For integration help, limits or quotas write to the Zoony team from zoonyiraq.com.