Kurir.Express API
Version 1.0.0 · openapi.json
Two APIs, one set of rules.
Partner API (/v1/partner/*, key prefix kek_) — place and track deliveries from your own system: e-commerce back office, warehouse software, restaurant POS.
Operator API (/v1/operator/*, key prefix kok_) — run your own courier company from your own dispatch software: see the offers and the open pool, answer them for a named courier, move orders through their lifecycle, confirm delivery, report positions and read the ledger. Everything behind it is the same machinery the Kurir.Express courier app uses — the API is a second transport for the same offers, not a second dispatch. An offer answered here carries the same offer_id the app would have shown, and whoever answers second gets a 409. Positions you report are what keeps a courier visible on the customer's map and findable by dispatch, so a courier you stop reporting for falls asleep on the same timeout as one whose phone went dark.
Authentication. Every request carries Authorization: Bearer <api key>. Keys are issued by Kurir.Express staff in the admin console; the secret is shown once, at creation, and stored only as a hash. A revoked key stops working on the very next request. A key of the wrong kind gets 403 — merchant keys have no access to /v1/operator/*, operator keys have none to /v1/partner/*. An operator key is bound to one company (country + tax id) and can see and act for that company's activated couriers only; anyone else's courier or order answers 404, never 403: we do not confirm that they exist.
Rate limit. Requests are counted per key in a sliding one-minute window. The default is a platform setting (60/min at the time of writing) and can be raised per key. Over the limit you get 429 with a Retry-After header.
Idempotency. Send Idempotency-Key on every creating call. A repeat with the same key and the same body returns the stored response instead of creating a second delivery.
Money. All amounts are integers in the minor unit of the order currency (cents for EUR). The platform does not process card payments: you settle with the courier directly, exactly as a private customer would.
Pricing and bidding. Services the platform prices itself (documents, parcel) are quoted through POST /v1/partner/quotes, and the signed quote_token must be passed to order creation. Everything else — pallets, trucks, moves, and any cross-border pair — has no platform price: POST /v1/partner/quotes answers 422 SERVICE_NOT_QUOTABLE, the order is created without a token, and couriers name the price. Either watch the bid.received webhook and accept a bid, or set auto_accept_max_price_minor and let the first bid at or below your ceiling win automatically.
Webhooks. Configure endpoints through your Kurir.Express contact. Each delivery carries X-Kurir-Signature: t=<unix>,v1=<hex hmac-sha256(secret, t + "." + body)>, X-Kurir-Event and X-Kurir-Delivery. Verify the signature and reject timestamps older than five minutes. Any 2xx counts as delivered; failures are retried on a schedule (1, 5, 30, 120, 480 minutes by default) and then marked dead. Polling the order endpoints is always a valid alternative to webhooks.
Merchant keys can subscribe to: order.status, courier.assigned, bid.received, order.delivered, order.failed, order.cancelled. Operator keys can subscribe to: offer.new, offer.result, order.update, order.cancelled_by_client, balance.low. offer.new and offer.result are the same frames the courier app receives, and balance.low fires when a commission charge takes a courier below the country minimum — below it, dispatch stops offering them work.
Fleet
GET /v1/operator/couriers
List your couriers
Every active courier registered on the platform under your company tax id, with the vehicle, the online toggle, the last known position and the orders they are currently holding. Couriers who have not been activated by the platform are not listed: you cannot act for them either.
Responses
Example
curl https://api.kurir.express/v1/operator/couriers -H 'Authorization: Bearer kok_YOUR_KEY'
Offers
GET /v1/operator/offers
Live offers and the open pool
Two lists. offers are live, addressed offers your couriers are being asked to answer right now — the very same offers the courier app would show, with the remaining ttl_ms, not a fresh countdown. pool is the open showcase in each courier's country and dispatch radius, with courier_ids telling you which of your couriers can take each order. Answering an offer is a decision on a timer; taking from the pool is not.
Responses
Example
curl https://api.kurir.express/v1/operator/offers -H 'Authorization: Bearer kok_YOUR_KEY'
POST /v1/operator/offers/{id}/accept
Accept an offer for one of your couriers
Takes the order at the offered price, exactly as the courier tapping "Accept" would. The offer is one and the same object on both transports: if the courier answered from the app a moment earlier, you get 409 — and so would they, had you been first.
Parameters
| Name | Type | |
|---|---|---|
idrequired |
string path | Offer id, exactly as it was delivered to the courier (webhook offer.new, or GET /v1/operator/offers). |
Idempotency-Key |
string header | Repeat-safe token chosen by you. Replaying the same key with the same body returns the stored response (header Idempotent-Replay: true); the same key with a different body is rejected with 422 IDEMPOTENCY_MISMATCH. |
Request body
OperatorOfferAnswerResponses
| Field | Type | |
|---|---|---|
offer_id |
string | |
order_id |
string | |
outcome |
string |
Example
curl -X POST https://api.kurir.express/v1/operator/offers/OFFER_ID/accept -H 'Content-Type: application/json' -d '{"courier_id":"COURIER_ID"}' -H 'Authorization: Bearer kok_YOUR_KEY'
POST /v1/operator/offers/{id}/decline
Decline an offer
The next candidate is offered the order on the same dispatch tick. Declining counts against the courier's acceptance rate exactly as it does in the app — the API is a second transport, not a second rulebook.
Parameters
| Name | Type | |
|---|---|---|
idrequired |
string path | Offer id, exactly as it was delivered to the courier (webhook offer.new, or GET /v1/operator/offers). |
Idempotency-Key |
string header | Repeat-safe token chosen by you. Replaying the same key with the same body returns the stored response (header Idempotent-Replay: true); the same key with a different body is rejected with 422 IDEMPOTENCY_MISMATCH. |
Request body
OperatorOfferAnswerResponses
| Field | Type | |
|---|---|---|
offer_id |
string | |
order_id |
string | |
outcome |
string |
Example
curl -X POST https://api.kurir.express/v1/operator/offers/OFFER_ID/decline -H 'Content-Type: application/json' -d '{"courier_id":"COURIER_ID"}' -H 'Authorization: Bearer kok_YOUR_KEY'
POST /v1/operator/offers/{id}/counter
Name your own price for an offer
Same rule as in the app: a price at or below the platform price assigns the order to your courier immediately, a higher one goes to the customer as a bid. Services without a platform price (pallets, trucks, moves) and any cross-border pair are priced this way only.
Parameters
| Name | Type | |
|---|---|---|
idrequired |
string path | Offer id, exactly as it was delivered to the courier (webhook offer.new, or GET /v1/operator/offers). |
Idempotency-Key |
string header | Repeat-safe token chosen by you. Replaying the same key with the same body returns the stored response (header Idempotent-Replay: true); the same key with a different body is rejected with 422 IDEMPOTENCY_MISMATCH. |
Request body
OperatorCounterRequestResponses
| Field | Type | |
|---|---|---|
offer_id |
string | |
order_id |
string | |
bid_id |
string |
Example
curl -X POST https://api.kurir.express/v1/operator/offers/OFFER_ID/counter -H 'Content-Type: application/json' -d '{"courier_id":"COURIER_ID","price_minor":4500,"eta_pickup_min":25}' -H 'Authorization: Bearer kok_YOUR_KEY'
POST /v1/operator/pool/{orderId}/claim
Take an order from the open pool
Assigns a pool order to your courier at the platform price, the same way the courier would from the showcase. The race is settled by the database, so two operators claiming the same order end with exactly one winner and one 409.
Parameters
| Name | Type | |
|---|---|---|
orderIdrequired |
string path | Order id from the pool list. |
Idempotency-Key |
string header | Repeat-safe token chosen by you. Replaying the same key with the same body returns the stored response (header Idempotent-Replay: true); the same key with a different body is rejected with 422 IDEMPOTENCY_MISMATCH. |
Request body
OperatorOfferAnswerResponses
| Field | Type | |
|---|---|---|
order_id |
string |
Example
curl -X POST https://api.kurir.express/v1/operator/pool/ORDER_ID/claim -H 'Content-Type: application/json' -d '{"courier_id":"COURIER_ID"}' -H 'Authorization: Bearer kok_YOUR_KEY'
Fleet orders
GET /v1/operator/orders
Orders your couriers are holding
Newest first, across the whole company. Page with cursor from next_cursor; a null next_cursor means the last page. cross_border and dropoff_country_code are here because a dispatcher decides who to send before the courier ever sees the order.
Parameters
| Name | Type | |
|---|---|---|
status |
string query | Filter by order status. |
cursor |
string query | Opaque cursor from the previous page. |
limit |
integer query |
Responses
Example
curl 'https://api.kurir.express/v1/operator/orders?status=PICKED_UP' -H 'Authorization: Bearer kok_YOUR_KEY'
GET /v1/operator/orders/{id}
Get one order
An order held by one of your couriers. An order held by someone else answers 404, not 403: we do not confirm that another company's order exists.
Parameters
| Name | Type | |
|---|---|---|
idrequired |
string path | Platform order id returned by POST /v1/partner/orders. |
Responses
Example
curl https://api.kurir.express/v1/operator/orders/ORDER_ID -H 'Authorization: Bearer kok_YOUR_KEY'
POST /v1/operator/orders/{id}/status
Move an order through its lifecycle
The same state machine, the same guards and the same side effects as the courier app: illegal transitions answer 409, FAILED requires a reason, PICKED_UP sends the recipient their delivery PIN by SMS. Delivery itself has its own endpoint (see /pod) because it needs proof.
Parameters
| Name | Type | |
|---|---|---|
idrequired |
string path | Platform order id returned by POST /v1/partner/orders. |
Idempotency-Key |
string header | Repeat-safe token chosen by you. Replaying the same key with the same body returns the stored response (header Idempotent-Replay: true); the same key with a different body is rejected with 422 IDEMPOTENCY_MISMATCH. |
Request body
OperatorStatusRequestResponses
| Field | Type | |
|---|---|---|
id |
string | |
status |
string | |
from |
string |
Example
curl -X POST https://api.kurir.express/v1/operator/orders/ORDER_ID/status -H 'Content-Type: application/json' -d '{"courier_id":"COURIER_ID","to":"PICKED_UP","lat":42.4304,"lng":19.2594}' -H 'Authorization: Bearer kok_YOUR_KEY'
POST /v1/operator/orders/{id}/pod
Confirm delivery
Proof of delivery: either the four-digit pin the recipient received by SMS, or photo_upload_id — the key of a photo uploaded through POST /v1/uploads. Coordinates are optional: the last position you reported for that courier is used, and the country's geofence radius is checked against it exactly as it is for the app. Delivery is where the platform commission is charged.
Parameters
| Name | Type | |
|---|---|---|
idrequired |
string path | Platform order id returned by POST /v1/partner/orders. |
Idempotency-Key |
string header | Repeat-safe token chosen by you. Replaying the same key with the same body returns the stored response (header Idempotent-Replay: true); the same key with a different body is rejected with 422 IDEMPOTENCY_MISMATCH. |
Request body
OperatorPodRequestResponses
| Field | Type | |
|---|---|---|
id |
string | |
status |
string | |
from |
string |
Example
curl -X POST https://api.kurir.express/v1/operator/orders/ORDER_ID/pod -H 'Content-Type: application/json' -d '{"courier_id":"COURIER_ID","pin":"1234"}' -H 'Authorization: Bearer kok_YOUR_KEY'
Telemetry
POST /v1/operator/locations
Report courier positions
A batch of track points. Every point takes the same path as a position frame from the app: it keeps the courier present for dispatch, feeds the live map the customer watches, and refreshes "alive". This is also what puts a courier online in the first place — a courier you stop reporting for falls asleep on the same timeout as one whose phone went dark. The batch size limit is a platform setting (200 at the time of writing) and is reported in the 422 body when exceeded.
Request body
OperatorLocationsRequestResponses
| Field | Type | |
|---|---|---|
accepted |
integer | |
couriers |
array |
Example
curl -X POST https://api.kurir.express/v1/operator/locations -H 'Content-Type: application/json' -d '{"locations":[{"courier_id":"COURIER_ID","lat":42.4304,"lng":19.2594,"ts":1788643648000}]}' -H 'Authorization: Bearer kok_YOUR_KEY'
Ledger
GET /v1/operator/ledger
Read your couriers' ledger
Platform commission charged on delivery, cash-on-delivery liabilities, top-ups and adjustments, newest first. Read-only: money is moved by people in the console, not by an integration. A positive amount grows the courier's position, a negative one shrinks it.
Parameters
| Name | Type | |
|---|---|---|
courier_id |
string query | One courier; omit for the whole company. |
before |
string query | Cursor: next_before from the previous page. |
limit |
integer query |
Responses
Example
curl 'https://api.kurir.express/v1/operator/ledger?limit=20' -H 'Authorization: Bearer kok_YOUR_KEY'
Quotes
POST /v1/partner/quotes
Quote a delivery
Returns a price and a signed quote_token valid for a few minutes. Services without a platform price (and any cross-border pair) answer 422 SERVICE_NOT_QUOTABLE with details.quoting = "bids" — create the order without a token and let couriers bid.
Request body
QuoteRequestResponses
Example
curl -X POST https://api.kurir.express/v1/partner/quotes \
-H 'Authorization: Bearer kek_YOUR_KEY' \
-H 'Content-Type: application/json' \
-d '{"service":"EXPRESS","delivery_service":"parcel","from":{"lat":42.4304,"lng":19.2594},"to":{"lat":42.4425,"lng":19.2762},"parcel":{"weight_kg":2,"fragile":false},"payment_methods":["cash"]}'
Orders
POST /v1/partner/orders
Create an order
Creates a normal Kurir.Express delivery owned by the account behind your key. Pass quote_token for a quoted service, or omit it and pass delivery_service for bidding. external_ref is your own order number: it is required and must be unique per key.
Parameters
| Name | Type | |
|---|---|---|
Idempotency-Key |
string header | Repeat-safe token chosen by you. Replaying the same key with the same body returns the stored response (header Idempotent-Replay: true); the same key with a different body is rejected with 422 IDEMPOTENCY_MISMATCH. |
Request body
PartnerCreateOrderRequestResponses
Example
curl -X POST https://api.kurir.express/v1/partner/orders \
-H 'Authorization: Bearer kek_YOUR_KEY' \
-H 'Idempotency-Key: shop-order-1042' \
-H 'Content-Type: application/json' \
-d '{"external_ref":"1042","quote_token":"<from /v1/partner/quotes>","pickup":{"location":{"lat":42.4304,"lng":19.2594},"contact":{"name":"Shop","phone":"+38269000003"}},"dropoff":{"location":{"lat":42.4425,"lng":19.2762},"contact":{"name":"Ana","phone":"+38269000004"}},"parcel":{"weight_kg":2,"fragile":false},"payment_methods":["cash"]}'
GET /v1/partner/orders
List your orders
Newest first. Only orders created through this key are listed. Page with cursor from next_cursor; a null next_cursor means the last page.
Parameters
| Name | Type | |
|---|---|---|
external_ref |
string query | Exact match on your own order number. |
status |
string query | Filter by order status. |
cursor |
string query | Opaque cursor from the previous page. |
limit |
integer query |
Responses
| Field | Type | |
|---|---|---|
items |
array | |
next_cursor |
string |
Example
curl 'https://api.kurir.express/v1/partner/orders?status=SEARCHING' -H 'Authorization: Bearer kek_YOUR_KEY'
POST /v1/partner/orders/batch
Create several orders at once
Best-effort batch: each element succeeds or fails on its own, and one bad address does not cancel the other nineteen deliveries. The response mirrors the request order. The maximum batch size is a platform setting (50 at the time of writing) and is reported in the 422 body when exceeded. Best-effort covers business errors only: a malformed element — one missing the required external_ref, say — rejects the whole request with 422, because the request itself did not parse.
Parameters
| Name | Type | |
|---|---|---|
Idempotency-Key |
string header | Repeat-safe token chosen by you. Replaying the same key with the same body returns the stored response (header Idempotent-Replay: true); the same key with a different body is rejected with 422 IDEMPOTENCY_MISMATCH. |
Request body
| Field | Type | |
|---|---|---|
ordersrequired |
array |
Responses
| Field | Type | |
|---|---|---|
items |
array | |
created |
integer | |
failed |
integer |
Example
curl -X POST https://api.kurir.express/v1/partner/orders/batch -H 'Authorization: Bearer kek_YOUR_KEY' -H 'Content-Type: application/json' -d '{"orders":[…]}'
GET /v1/partner/orders/{id}
Get one order
Full card of your order, including the courier once assigned and a freshly signed pod_photo_url (valid for 15 minutes) once the delivery is photographed. Someone else's order answers 404, not 403.
Parameters
| Name | Type | |
|---|---|---|
idrequired |
string path | Platform order id returned by POST /v1/partner/orders. |
Responses
Example
curl https://api.kurir.express/v1/partner/orders/ORDER_ID -H 'Authorization: Bearer kek_YOUR_KEY'
POST /v1/partner/orders/{id}/cancel
Cancel an order
Allowed while the order has not been handed over. The state machine decides: a delivered order answers 409 ILLEGAL_TRANSITION.
Parameters
| Name | Type | |
|---|---|---|
idrequired |
string path | Platform order id returned by POST /v1/partner/orders. |
Request body
| Field | Type | |
|---|---|---|
reason |
string |
Responses
| Field | Type | |
|---|---|---|
id |
string | |
status |
string | |
cancel_reason |
string |
Example
curl -X POST https://api.kurir.express/v1/partner/orders/ORDER_ID/cancel -H 'Authorization: Bearer kek_YOUR_KEY'
Bidding
GET /v1/partner/orders/{id}/bids
List open bids
Courier offers for an order that has no platform price. Cheapest first; expired bids are not shown.
Parameters
| Name | Type | |
|---|---|---|
idrequired |
string path | Platform order id returned by POST /v1/partner/orders. |
Responses
| Field | Type | |
|---|---|---|
items |
array | |
order_price_cents |
integer | |
currency |
string |
Example
curl https://api.kurir.express/v1/partner/orders/ORDER_ID/bids -H 'Authorization: Bearer kek_YOUR_KEY'
POST /v1/partner/orders/{id}/bids/{bidId}/accept
Accept a bid
Assigns the courier who made this bid; the bid price becomes the order price. Losing the race to another courier answers 409 OFFER_LOST, an expired bid 409 OFFER_EXPIRED.
Parameters
| Name | Type | |
|---|---|---|
idrequired |
string path | Platform order id returned by POST /v1/partner/orders. |
bidIdrequired |
string path |
Responses
| Field | Type | |
|---|---|---|
order_id |
string | |
bid_id |
string | |
price_cents |
integer | |
eta_pickup_min |
integer | |
status |
string |
Example
curl -X POST https://api.kurir.express/v1/partner/orders/ORDER_ID/bids/BID_ID/accept -H 'Authorization: Bearer kek_YOUR_KEY'
Tracking
GET /v1/partner/orders/{id}/track
Track an order
Status, courier (with phone while the order is live), live position while the courier physically holds the parcel, ETAs and a short public tracking link you can forward to the recipient.
Parameters
| Name | Type | |
|---|---|---|
idrequired |
string path | Platform order id returned by POST /v1/partner/orders. |
Responses
| Field | Type | |
|---|---|---|
order_id |
string | |
external_ref |
string | |
public_code |
string | |
status |
string | |
courier |
object | |
location |
object | |
eta |
object | |
track_url |
string |
Example
curl https://api.kurir.express/v1/partner/orders/ORDER_ID/track -H 'Authorization: Bearer kek_YOUR_KEY'
Schemas
Error
| Field | Type | |
|---|---|---|
coderequired |
string | |
messagerequired |
string | |
details |
object |
Order
An order as the platform sees it. Same shape the mobile app receives.
| Field | Type | |
|---|---|---|
id |
string | |
public_code |
string | |
external_ref |
string | Your own order number. Required on create, unique per API key. |
status |
string | |
delivery_service |
string | |
price_cents |
integer | Minor units of currency. Zero means "not priced yet" (bidding). |
currency |
string | |
courier |
object | |
auto_accept_max_price_minor |
integer | |
created_at |
string |
QuoteRequest
| Field | Type | |
|---|---|---|
servicerequired |
string | |
delivery_service |
string | |
fromrequired |
object | |
torequired |
object | |
parcelrequired |
object | |
cod_cents |
integer | |
payment_methodsrequired |
array | |
scheduled_for |
string |
QuoteResponse
| Field | Type | |
|---|---|---|
quote_tokenrequired |
string | |
price_centsrequired |
integer | |
payout_centsrequired |
integer | |
currencyrequired |
string | |
routed_kmrequired |
number | |
min_vehiclerequired |
string | |
breakdownrequired |
object | |
expires_atrequired |
string | |
warnings |
array |
PartnerCreateOrderRequest
Exactly one of quote_token (quoted services) or delivery_service (bidding) is required — a rule that JSON Schema cannot express, so it is enforced by the server and stated here.
| Field | Type | |
|---|---|---|
quote_token |
string | |
delivery_service |
string | |
pickuprequired |
object | |
dropoffrequired |
object | |
parcel |
object | |
cod_cents |
integer | |
payment_methodsrequired |
array | |
client_notes |
string | |
scheduled_pickup_at |
string | |
scheduled_dropoff_at |
string | |
external_refrequired |
string | |
auto_accept_max_price_minor |
integer |
WebhookEvent
Body of every webhook delivery. Which events an endpoint may subscribe to depends on the key: merchant keys get order.status, courier.assigned, bid.received, order.delivered, order.failed, order.cancelled; operator keys get offer.new, offer.result, order.update, order.cancelled_by_client, balance.low.
| Field | Type | |
|---|---|---|
event |
string | |
occurred_at |
string | |
data |
object |
OperatorCouriers
| Field | Type | |
|---|---|---|
couriersrequired |
array |
OperatorOffers
| Field | Type | |
|---|---|---|
offersrequired |
array | |
poolrequired |
array |
OperatorOrder
| Field | Type | |
|---|---|---|
idrequired |
string | |
public_coderequired |
string | |
courier_idrequired |
string | |
statusrequired |
string | |
payout_minorrequired |
integer | |
cod_minorrequired |
integer | |
currencyrequired |
string | |
pickuprequired |
object | |
dropoffrequired |
object | |
dropoff_country_coderequired |
string | |
cross_borderrequired |
boolean | |
recipientrequired |
object | |
created_atrequired |
string | |
assigned_atrequired |
string | |
delivered_atrequired |
string |
OperatorOrders
| Field | Type | |
|---|---|---|
itemsrequired |
array | |
next_cursorrequired |
string |
OperatorLedger
| Field | Type | |
|---|---|---|
itemsrequired |
array | |
next_beforerequired |
string |
OperatorOfferAnswer
| Field | Type | |
|---|---|---|
courier_idrequired |
string | |
reason |
string |
OperatorCounterRequest
| Field | Type | |
|---|---|---|
courier_idrequired |
string | |
price_minorrequired |
integer | |
eta_pickup_minrequired |
integer |
OperatorStatusRequest
| Field | Type | |
|---|---|---|
courier_idrequired |
string | |
torequired |
string | |
lat |
number | |
lng |
number | |
accuracy_m |
number | |
failure_reason |
string | |
note |
string |
OperatorPodRequest
Exactly one of pin or photo_upload_id is required — a rule JSON Schema cannot express, so it is enforced by the server and stated here.
| Field | Type | |
|---|---|---|
courier_idrequired |
string | |
pin |
string | |
photo_upload_id |
string | |
lat |
number | |
lng |
number | |
note |
string |
OperatorLocationsRequest
| Field | Type | |
|---|---|---|
locationsrequired |
array |