Integration guide for developers

Tadalal Delivery API

Send an order from your own website straight into the merchant’s Tadalal account. One POST per order; a driver is dispatched automatically.

Version v1 Base https://api.tadalal.app/api/v1 Region Kuwait only Currency KWD, 3 decimals

How it works

A customer checks out on your website. Your server posts the delivery details to tadalal. We price it, charge the merchant’s Tadalal wallet when the driver collects, and dispatch. The order appears in the merchant’s Orders page exactly as if it had been typed there by hand.

  1. 1 Your customer places an order Your site handles the products, the payment and the receipt. Tadalal never sees any of that.
  2. 2 Your server calls POST /orders Send the recipient, their phone and where it is going. That is the whole required payload.
  3. 3 We dispatch and you poll for status Use the returned id to show live progress on your own order-tracking page.
Note

There are no webhooks yet — status is read by polling GET /orders/{id}. Once every 30–60 seconds while an order is active is plenty; anything faster will hit the rate limit without telling you anything new.

Before you start

Four things must be true, and three of them are set up by the merchant, not by you.

  • An API key. The merchant creates it in their Tadalal portal under API keys and sends it to you privately. It is shown once and cannot be recovered.
  • The branch has a map location. Orders are collected from a branch, and a branch with no pin cannot be a pickup point. Portal → Branches.
  • The wallet has money in it. An order is refused with INSUFFICIENT_BALANCE if the balance will not cover the delivery fee.
  • You call us from a server. Never from browser JavaScript, a mobile app, or anywhere a customer could read the key.

Authentication

Every request carries the key as a bearer token. Keys look like glk_live_3f9a2b71_… — the part before the last underscore identifies the key, the rest is the secret.

Header
Authorization: Bearer glk_live_3f9a2b71_Kj8sQ2vX...
Content-Type: application/json
Keep it secret

The key can create deliveries that spend the merchant’s money. Store it in an environment variable or your platform’s secrets store — never in Git, never in a theme file, never in anything sent to a browser.

If it leaks, the merchant revokes it in the portal and it stops working on the very next request. Then they issue a new one.

Every response has the same shape

Success
{
  "success": true,
  "data": { /* … */ }
}
Failure
{
  "success": false,
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "…"
  }
}

Branch on error.code, never on error.message — the wording may be improved at any time, the codes will not change.

Test your key

Run this first, before writing any order code. It confirms the key, the base URL and the pickup branch in one call — which is the answer to nearly every “it isn’t working” question.

GET /api/v1/ping
Request
curl https://api.tadalal.app/api/v1/ping \
  -H "Authorization: Bearer $TADALAL_API_KEY"
Response
{
  "success": true,
  "data": {
    "authenticated": true,
    "merchant": { "id": "…", "name": "Sharq Sweets" },
    "api_key": {
      "name": "Main website",
      "scopes": ["orders:write", "orders:read"],
      "pickup_branch_id": "b3f1…"
    },
    "wallet": { "balance": 412.000, "currency": "KWD" },
    "branches": [
      { "id": "b3f1…", "name": "Sharq", "located": true }
    ],
    "server_time": "2026-09-03T09:14:02.881Z"
  }
}

If any branch shows "located": false, ask the merchant to set its map pin before you send orders that collect from it.

Create an order

The one endpoint you genuinely need. Three fields are required.

POST /api/v1/orders

Fields

FieldTypeNotes
recipient_name * string Who receives the parcel.
recipient_phone * string Kuwait mobile, 8 digits, e.g. 50123456. The driver calls this number.
delivery_address * string Free text. Include block, street and building — see Addresses.
delivery_lat
delivery_lng
number

Strongly recommended. If you have a map picker at checkout, send the pin.

Without them we geocode the address text, which can fail on a vague one.

external_ref string

Your own order number. Echoed back on every read, and you can look the order up by it.

Unique per merchant — a second order with the same value is refused. That is your safety net against duplicates.

branch_id uuid Required only when the merchant has more than one branch and the key is not bound to one.
building_number string Shown to the driver alongside the address.
city string Area or governorate, e.g. Salmiya.
notes string Instructions for the driver — “second floor, ring twice”.
scheduled_at ISO 8601 Requested delivery time. Advisory, not a guarantee.

* required. Pickup is never sent — it comes from the branch.

Example

Request
curl -X POST https://api.tadalal.app/api/v1/orders \
  -H "Authorization: Bearer $TADALAL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: web-order-10482" \
  -d '{
    "recipient_name": "Fatima Al-Sabah",
    "recipient_phone": "50123456",
    "delivery_address": "Salmiya, Block 10, Street 1, Building 27",
    "delivery_lat": 29.3328,
    "delivery_lng": 48.0672,
    "city": "Salmiya",
    "notes": "Second floor, ring twice",
    "external_ref": "web-order-10482"
  }'
Response · 201 Created
{
  "success": true,
  "data": {
    "id": "7c1e4f8a-2b93-4d61-9f0a-55d2c1e77b31",
    "order_number": "ORD-260903-4821",
    "external_ref": "web-order-10482",
    "status": "pending",
    "recipient": { "name": "Fatima Al-Sabah", "phone": "50123456" },
    "delivery": {
      "address": "Salmiya, Block 10, Street 1, Building 27",
      "lat": 29.3328, "lng": 48.0672
    },
    "pickup": { "address": "Sharq, Ahmad Al Jaber St", "lat": 29.3759, "lng": 47.9774 },
    "distance_km": 8.42,
    "delivery_fee": 1.750,
    "currency": "KWD",
    "driver": null,
    "placed_at": "2026-09-03T09:14:02.881Z"
  }
}

Store data.id against your own order. It is what you poll with.

Addresses & coordinates

The delivery fee is calculated from the driving distance between the branch and the drop-off. A wrong coordinate is not a cosmetic problem — it produces a wrong fare and sends a driver to the wrong place.

Send coordinates when you have them

If your checkout has a map picker, send delivery_lat and delivery_lng. Nothing we can infer from text beats a pin the customer dropped themselves.

When you send no coordinates we look the address up. That lookup is deliberately strict: a vague address is rejected with ADDRESS_NOT_RESOLVED rather than quietly resolved to the middle of the country — which is what produces a plausible-looking 53 km delivery across the street.

Resolves
"Salmiya, Block 10, Street 1, Building 27"
"Sharq, Ahmad Al Jaber Street, Kuwait City"
"Hawally, Block 4, Tunis Street"
Rejected
"Kuwait"
"near the big mosque"
"Salmiya"          // area alone

Deliveries are Kuwait-only. A coordinate outside the country is refused with INVALID_DELIVERY_COORDS.

Avoiding duplicates

This is the part worth reading twice. If your request times out you do not know whether the order was created — and retrying blindly is how one customer gets two drivers and the merchant pays two fees.

Send an Idempotency-Key

Put a value that is unique to that order — your own order id works perfectly — in an Idempotency-Key header. Reuse the same value on every retry of that order.

Header
Idempotency-Key: web-order-10482

The first request does the work. Any repeat with the same key returns the original response, byte for byte, with an Idempotent-Replay: true header. No second order, no second charge.

One key, one order

Generate the key per order, not per customer and not per day. Sending the same key with a different body is refused with IDEMPOTENCY_KEY_REUSED — because the alternative would be to silently swallow a real second delivery.

Keys are remembered for 24 hours. After that the same value is treated as a new request, which is correct: a retry a day later is not a retry.

The second safety net

Send external_ref as well. It is unique per merchant, so even with no idempotency key at all, replaying the same website order is refused with DUPLICATE_EXTERNAL_REF. Use both and duplicates are effectively impossible.

Which branch the order is collected from

Pickup always comes from a branch — you never send a pickup address.

The merchant hasWhat happens
One branch Used automatically. Send nothing.
Several, key bound to one The key’s branch is used. Send nothing. Recommended.
Several, key not bound You must send branch_id, or the request is refused with BRANCH_REQUIRED — the response lists the branches to choose from.

The simplest setup for a multi-branch merchant is one API key per branch, each bound to its own. Then your code never mentions a branch at all.

Order status

Six values, in the order a normal delivery passes through them. Treat any value you do not recognise as “in progress” — new ones may be added.

pendingReceived and waiting for a driver.
assignedA driver has it and is heading to the shop.
picked_upCollected. The delivery fee is charged at this point.
in_transitOn the way to your customer.
deliveredHanded over. Final.
failedCould not be delivered — nobody home, wrong address.
returningComing back to the shop.
returnedBack at the shop. Final.
cancelledCancelled before collection. Final.
Note

These are the only status values the API will ever return. Tadalal uses a more detailed set internally; it is deliberately not exposed, so we can change it without breaking your site.

Read an order

GET /api/v1/orders/{reference}

{reference} can be any of three things, so you never have to store an identifier you did not choose:

ReferenceExample
Our order id7c1e4f8a-2b93-4d61-9f0a-55d2c1e77b31
Our order numberORD-260903-4821
Your own referenceweb-order-10482
Request
curl https://api.tadalal.app/api/v1/orders/web-order-10482 \
  -H "Authorization: Bearer $TADALAL_API_KEY"

The response is the same order object as on create. Once a driver is assigned, driver carries their name and phone so you can show it to your customer.

Response · while in transit
{
  "success": true,
  "data": {
    "order_number": "ORD-260903-4821",
    "status": "in_transit",
    "driver": { "name": "Ahmed K.", "phone": "66554433" },
    "picked_up_at": "2026-09-03T09:41:20.115Z",
    "delivered_at": null
  }
}

List orders

GET /api/v1/orders

Newest first. Useful for reconciliation, not for tracking a single order.

QueryDefaultNotes
limit25Maximum 100. Larger values are clamped, not rejected.
offset0For paging.
status—One of the values in Order status.
created_after—ISO 8601 timestamp.
Request
curl "https://api.tadalal.app/api/v1/orders?status=in_transit&limit=50" \
  -H "Authorization: Bearer $TADALAL_API_KEY"

Cancel an order

POST /api/v1/orders/{reference}/cancel

Works only while the order is pending or assigned. Once the driver has collected the parcel the fee has been charged and someone is carrying it, so cancellation becomes a support conversation rather than an API call — you get NOT_CANCELLABLE.

Cancelling an already-cancelled order returns 200, not an error, so a retry is safe.

Errors

Every failure carries a stable error.code. These are the ones worth handling explicitly; anything else can be logged and retried.

CodeHTTPWhat to do
INSUFFICIENT_BALANCE402 Tell the shop owner to top up their Tadalal wallet. Do not retry until they have.
MISSING_FIELDS400 error.details.missing lists exactly which. A bug in your payload.
ADDRESS_NOT_RESOLVED422 Ask the customer for a more precise address, or send coordinates.
INVALID_DELIVERY_COORDS422 The point is outside Kuwait. Check you have not swapped lat and lng.
DUPLICATE_EXTERNAL_REF409 This order already exists. Safe to treat as success.
IDEMPOTENCY_KEY_REUSED422 You sent one key for two different orders. Generate one per order.
REQUEST_IN_PROGRESS409 An identical request is still running. Wait a few seconds and retry the same call.
BRANCH_REQUIRED409 Send branch_id. The response lists the options.
BRANCH_NOT_LOCATED409 The merchant must set the branch’s map pin in the portal.
INVALID_API_KEY401 Wrong, revoked, or the merchant’s account is inactive. Do not retry.
INSUFFICIENT_SCOPE403 The key is read-only. Ask for one with orders:write.
NOT_CANCELLABLE409 Already collected. Contact support.
RATE_LIMITED429 Back off and retry after the window resets.
GEOCODING_UNAVAILABLE503 Temporary. Retry with the same Idempotency-Key.
Retrying safely

On a timeout or a 5xx, retry the same request with the same Idempotency-Key. That is the only retry that cannot create a duplicate. Never retry a 4xx unchanged — it will fail identically.

Rate limits

OperationLimit
Creating and cancelling orders60 / minute per key
Reading and listing300 / minute per key

Standard RateLimit-* headers are returned on every response. Polling one active order every 30 seconds uses about two requests a minute — well inside the limit.

Go-live checklist

  • GET /ping returns 200 from your production server, not just your laptop.
  • The key is in an environment variable and is not in your Git history.
  • Every create sends an Idempotency-Key and an external_ref.
  • You store data.id against your own order record.
  • INSUFFICIENT_BALANCE alerts the shop owner instead of failing silently. This is the most common real-world failure.
  • A failed order is visible to someone. A delivery that never reached us should raise an alarm on your side, not sit in a log.
  • One real order end to end — placed, collected, delivered — before you switch customers over.