Creating orders
One order in your system becomes one order in Zoopit. This page covers creating it, retrying safely, changing it and cancelling it. The field-by-field schema is in the generated reference: Create an order.
Create an order
POST https://orders-api.cloud.zoopit.no/v1/orders
Authorization: Bearer <token>
Content-Type: application/json
A typical shop delivery: picked up at the shop, delivered to a recipient within a time window,
with your own order number as reference_id.
{
"owner": "<your project id>",
"reference_id": "100234",
"amounts": { "colli": 1 },
"attributes": { "erp_store": "majorstuen" },
"tasks": [
{
"type": "pickup",
"amounts": { "colli": 1 },
"places": [{
"duration": 2,
"time_windows": [{ "start": "2026-12-04T08:00:00+01:00", "end": "2026-12-04T11:00:00+01:00" }],
"location": {
"name": "Example Florist Majorstuen",
"phone_number": "+4722000000",
"address": {
"country_code": "no",
"street_address": "Bogstadveien 1",
"zip_code": "0355",
"city": "Oslo"
}
}
}]
},
{
"type": "delivery",
"amounts": { "colli": 1 },
"places": [{
"duration": 3,
"time_windows": [{ "start": "2026-12-04T10:00:00+01:00", "end": "2026-12-04T14:00:00+01:00" }],
"location": {
"name": "Kari Nordmann",
"phone_number": "+4790000000",
"address": {
"country_code": "no",
"street_address": "Kirkeveien 1",
"zip_code": "0268",
"city": "Oslo"
}
}
}]
}
],
"delivery": {
"oneOf": [{
"location": {
"name": "Kari Nordmann",
"phone_number": "+4790000000",
"address": {
"country_code": "no",
"street_address": "Kirkeveien 1",
"zip_code": "0268",
"city": "Oslo"
}
}
}]
}
}
It returns 201 with the order in resource. Store resource.id next to your own order
number — it is the key for every later call and for webhooks.
What each part does:
| Field | Purpose |
|---|---|
owner | Your project id. Required. |
reference_id | Your order number. Shown in the dashboard, filterable, and returned in every status update and webhook. |
tasks | What gets planned. One pickup (the shop) and one delivery (the recipient). Planning and the driver app work from tasks. |
tasks[].places[].duration | Time spent at the stop, in minutes. |
tasks[].places[].time_windows | When the stop may happen. |
tasks[].amounts | What is carried. Give the pickup and the delivery the same amounts. |
delivery.oneOf[0].location | The recipient again, for recipient SMS. SMS templates read the recipient from here, not from tasks. |
attributes | Your own key–value data. |
tasksAn order with only pickup / delivery and no tasks is created without error and shows up
in lists, but planning never sees it: it produces routes with no stops. Send tasks.
Until SMS templates can read the recipient from tasks, send the recipient in
delivery.oneOf as well, as above.
Status on create
New orders are RESERVED (committed to be delivered, ready for planning) unless you send
"status": "CREATED". Use CREATED for an order that should exist in Zoopit but not be planned
yet, and PATCH it to RESERVED when it is ready. See
Statuses and reasons.
Addresses
A location needs at least one of name, phone_number or email, and an address in one of two
forms:
- Structured —
country_code,street_address,zip_code,city, optionallycoordinates({ "lat": …, "lon": … }). Preferred: it is geocoded more reliably. - Free text —
{ "country_code": "no", "search": "Kirkeveien 1, 0268 Oslo" }. The first geocoding match is used, so a wrong guess delivers to the wrong place without any error.
If you already hold reliable coordinates, send them and the address is not geocoded again.
location.comment.text attaches a note to that recipient's address, for example a door code.
It is stored on the address, so it also appears on later deliveries to the same recipient at the
same address. Put anything that only concerns this one order in attributes. Send a comment in
tasks only, not again in delivery, or it is stored twice.
Retries and duplicates
reference_id is not unique and there is no idempotency key. Sending the same order twice
creates two orders.
If a create times out or fails with a 5xx, look before you retry:
curl -s -G 'https://orders-api.cloud.zoopit.no/v1/orders' \
-H 'Authorization: Bearer <token>' \
--data-urlencode 'where=[{"reference_id":{"=":"100234"}}]' \
--data-urlencode 'select=["id","reference_id","status","created_at"]' \
--data-urlencode 'limit=10'
If an order with your reference already exists, keep its id instead of creating another. If a
duplicate slips through, cancel it with canceled.duplicate (below).
Read an order
GET /v1/orders/:order_id
Returns the order and, in related, its tasks, locations, route, activities and order actions.
To find an order by your own number, use the reference_id query above.
Update an order
PATCH /v1/orders/:order_id
{
"modified_at": "2026-12-03T14:02:11.123456+00:00",
"attributes": { "erp_store": "majorstuen", "card_printed": true }
}
modified_atis required. Send the value from your latest read; sendnullif the order has never been modified. If the order has changed since — a driver completed a stop, a dispatcher moved it — the update is refused with404 Not found or has been changed. Read the order again and retry.- Only the fields you send change. Fields you leave out keep their value.
- Each field you send is replaced whole.
attributes,tags,amounts,deliveryandpickupare not merged: send{"attributes": {"a": 1}}and every other attribute key is gone. Read the order, merge your change into the existing object, and send the whole object.
To change a stop's address or time window, change the task: PATCH /v1/tasks/:id — see
Update a task.
Do not set COMPLETED or FAILED yourself. Those are written as the driver works the route.
Cancel an order
Cancelling keeps the order and its history, and it shows up in your status updates. If the order is already planned, cancelling also takes its stops off the route, so the driver no longer sees them.
POST /v1/orders/:order_id/cancel
{
"status_reason": "canceled.by_customer"
}
The response is the cancelled order, the same as for GET /v1/orders/:order_id. You don't need
modified_at.
| Reason | When |
|---|---|
canceled.by_customer | Your customer called it off |
canceled.by_operator | You or the carrier called it off |
canceled.duplicate | The same delivery exists on another order |
canceled.other | Anything else; requires status_description |
{
"status_reason": "canceled.other",
"status_description": "Shop closed due to water damage"
}
A reason is required, and canceled.other without a description is refused with 400:
{ "request_id": "…", "message": "status_reason 'canceled.other' requires a non-empty status_description" }
What happens to a planned order
- Stops that are not done yet are taken off the route. The driver's app drops them on its next refresh, and the route's history in the dashboard records the cancellation with your reason.
- Stops that are already done stay on the route. For example, if the goods have been picked up and the delivery is still to come, the pickup stays and the delivery is removed. The goods are still on the vehicle, so tell the dispatcher what should happen to them.
- An order with no stops left on its route is taken off the route (
route_idbecomesnull).
When cancelling is refused
Cancelling returns 409 when the order:
- is already
CANCELED; - is
COMPLETED; - has been delivered (the delivery stop is done), even if its status has not caught up yet.
404 means the order does not exist or is not in a project you can reach.
Cancelling with PATCH
PATCH /v1/orders/:order_id with "status": "CANCELED" still works and follows the same rules,
with the same 409s and the same stops removed. Prefer the cancel endpoint: it needs no
modified_at, and it does not accept a cancellation without a reason.
A cancellation is final
A cancelled order cannot be reopened. A PATCH that moves a CANCELED order to any other status
returns 409. If the delivery is on again, create a new order.
Delete vs cancel
DELETE /v1/orders/:order_id removes the order from every list and search. It cannot be read
back, it does not appear in polling, and no webhook is sent. Use it only for orders created by
mistake in testing. For anything a customer or dispatcher may ask about later, cancel.