API documentation

Create deliveries, manage riders and follow every job to the door. The base address for every call on this page is https://YOUR-DOMAIN/api.

Quickstart

  1. Create an account, open Developers in the console and create an API key. It starts with dk_ and is shown once.
  2. Add a rider. The response includes that rider’s token, which their app uses.
    curl https://YOUR-DOMAIN/api/v1/workers \
      -H "Authorization: Bearer $FLEETIT_KEY" -H "Content-Type: application/json" \
      -d '{ "name": "Tendai Moyo", "phone": "+263771234567", "vehicle": "motorbike" }'
  3. Create a delivery. Dispatch starts looking for a rider at once.
    curl https://YOUR-DOMAIN/api/v1/jobs \
      -H "Authorization: Bearer $FLEETIT_KEY" -H "Content-Type: application/json" \
      -H "Idempotency-Key: order-1042" \
      -d '{
        "pickup": {
          "lat": -17.7572, "lng": 31.0853,
          "name": "Rustic Crust", "address": "Borrowdale Village"
        },
        "dropoff": {
          "lat": -17.7450, "lng": 31.1000,
          "name": "Tari", "phone": "+263771000002", "address": "12 Pendennis Rd"
        },
        "cash_to_collect": 2000,
        "external_ref": "order-1042"
      }'
  4. Give the customer the tracking_url from the response, and set a webhook to hear about each step.

Authentication

Send a bearer token on every request: Authorization: Bearer <token>. There are two kinds.

TokenStarts withUsed byHow you get it
API keydk_Your server. Full access to your account’s jobs, riders, settings and webhooks.Console → Developers. Up to 20; revoke any at any time.
Rider tokenwk_One rider’s app. Only the /v1/worker/… endpoints, only that rider’s jobs.Returned once when you add the rider; replace it with POST /v1/workers/{id}/token.

Keep API keys on your server. Never put one in a mobile app or a web page: anyone who has it can act as you. Keys cannot create other keys or change your team; that needs an owner signed in to the console.

Conventions

  • JSON in and out. Send Content-Type: application/json with a body.
  • Money is an integer in minor units: 2000 means $20.00.
  • Times on jobs and riders are RFC 3339 strings in UTC (2026-11-12T10:15:00Z). Webhook and tracking timestamps are Unix seconds.
  • Coordinates are WGS84 decimal degrees, lat then lng. Out-of-range values are rejected.
  • Ids are UUIDs that sort by creation time.
  • Lists return newest first. Pass the response’s next_before as before to get the next page.

Errors

Errors use standard status codes and always have this body. Branch on code, show message to a person.

{ "error": { "code": "invalid_transition", "message": "a job that is picked_up cannot be cancelled" } }
StatusMeaningWhat to do
400The request is malformed or a value is out of range.Fix the request; retrying unchanged will fail again.
401Missing, wrong or revoked token.Check the key and the Bearer prefix.
403The token is valid but not allowed to do this.Use an API key, or sign in as an owner.
404No such job, rider or link in your account.Check the id.
409Valid, but the current state rules it out: invalid_transition, offer_gone, worker_full, phone_taken, insufficient_cash and others.Re-read the job or rider and decide again.
429Too many requests.Wait the number of seconds in Retry-After.
5xxOur fault.Retry with backoff; use an idempotency key when creating jobs.

Idempotency

Networks fail after the server has acted. To make POST /v1/jobs safe to retry, send an Idempotency-Key header: any string up to 200 characters that is unique per order, such as your order number. The first request creates the job and answers 201. Any later request with the same key answers 200 with that same job and creates nothing.

Rate limits

Each API key may make 1,200 requests a minute and each rider token 600. Beyond that you get 429 with a Retry-After header giving the seconds until the minute rolls over. Limits are per token, so one busy integration does not slow another. Prefer webhooks to polling.

Job lifecycle

A job moves forward through these states. failed and cancelled are the other two ways it can end.

StateMeaningMoves on when
searchingWaiting for a rider; offers are going out.A rider accepts, or you assign one.
assignedA rider is heading to the pickup.The rider is within 60 m of the pickup, or reports arrival.
at_pickupThe rider is at the pickup.The rider confirms they have the goods.
picked_upThe goods are on their way.The rider is within 60 m of the drop-off, or reports arrival.
at_dropoffThe rider is at the door.The rider confirms delivery, or reports a failure.
deliveredDone. Proof and cash collected are recorded.Final.

You can cancel, or put a job back in the queue, until the rider has picked the goods up. After that the job can only be delivered or failed by the rider, so goods are never stranded with no job attached.

How a rider is chosen. Every second, all waiting jobs are matched to nearby riders together, in the pairing with the least total waiting. Riders are found by map hexagon around the pickup, timed by road, and ruled out if they are off duty, full, or would exceed their cash limit. A rider with room for two can be given a second order from the same pickup if it barely delays the first customer. If the goods will not be ready for a while (set ready_at, or let the system learn it per pickup place), the job is held so the rider arrives about when they are.

Webhooks

Set one HTTPS address in Console → Developers (or PUT /v1/webhook) and we POST every job event to it. Use Send test event to check your endpoint, and the delivery log to see what your server answered.

Events

job.created, job.offered, job.assigned, job.unassigned, job.at_pickup, job.picked_up, job.at_dropoff, job.delivered, job.failed, job.cancelled, and job.no_rider (sent once if nobody has taken a job after two minutes). The test button sends ping.

{
  "id": 48213,
  "type": "job.delivered",
  "job_id": "0196f3c2-6d1e-7c41-9a55-0d7f2b8e4a10",
  "created_at": 1794480000,
  "data": { "job": { "id": "0196f3c2-…", "state": "delivered", "cash_collected": 2000, "…": "the full Job object" } }
}

Delivery rules

  • Answer with any 2xx within 5 seconds. Do slow work after you answer.
  • Anything else is retried with growing gaps (10 s, 20 s, 40 s … up to an hour) for 12 attempts.
  • Delivery is at least once and not strictly ordered. De-duplicate on id, and treat the higher id as newer.

Verify the signature

Every request carries X-FleetIt-Signature: t=<unix seconds>,v1=<hex>. v1 is the HMAC-SHA256 of "<t>.<raw body>" using your signing secret. Compute it over the raw bytes you received, before parsing, and reject requests whose t is more than five minutes old.

// Node.js (Express)
import crypto from "node:crypto";

app.post("/dispatch-events", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("x-fleetit-signature") ?? "";
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", process.env.FLEETIT_WEBHOOK_SECRET)
    .update(`${t}.${req.body}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  const match = v1?.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
  if (!fresh || !match) return res.sendStatus(400);

  const event = JSON.parse(req.body);
  // … handle event.type, de-duplicating on event.id …
  res.sendStatus(204);
});
# Python (Flask)
import hashlib, hmac, os, time
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/dispatch-events")
def dispatch_events():
    parts = dict(p.split("=", 1) for p in request.headers.get("X-FleetIt-Signature", "").split(",") if "=" in p)
    t, v1 = parts.get("t", "0"), parts.get("v1", "")
    expected = hmac.new(os.environ["FLEETIT_WEBHOOK_SECRET"].encode(),
                        f"{t}.".encode() + request.get_data(), hashlib.sha256).hexdigest()
    if abs(time.time() - int(t)) > 300 or not hmac.compare_digest(v1, expected):
        abort(400)
    event = request.get_json()
    # … handle event["type"], de-duplicating on event["id"] …
    return "", 204

Building a rider app

A rider’s app talks to the /v1/worker/… endpoints with that rider’s token. The whole loop is:

  1. Go on duty: POST /v1/worker/duty with {"on_duty": true}.
  2. Send positions every 5 to 10 seconds: POST /v1/worker/locations. Buffer fixes when offline and send up to 200 at once. A rider who has not been heard from for 90 seconds stops receiving offers. The response lists any jobs this upload marked as arrived.
  3. Check for an offer: GET /v1/worker/offers. A rider has at most one at a time, with a countdown. Customer contact details are held back until they accept.
  4. Accept or decline: POST /v1/worker/offers/{id}/accept. A 409 offer_gone means it expired or went elsewhere.
  5. Follow the plan: GET /v1/worker/plan returns the stops in the order to visit them, including both pickups first when two orders are stacked.
  6. Confirm hand-overs: POST /v1/worker/jobs/{id}/progress with picked_up, then delivered (with proof and the cash collected) or failed with a reason. Arrivals are detected from position, so no tap is needed for those.

Offers are fetched by polling today; ask every few seconds while on duty.

Quotes and pricing

POST /v1/quotes with a pickup and drop-off returns the delivery fee, the road distance and how soon a rider could collect, without creating anything. Call it at checkout.

The fee is (fee_base + fee_per_km × km) × multiplier. The multiplier is 1.0 while free riders within about ten minutes of the pickup cover the orders waiting there, and rises in steps of 0.05 toward your surge_max as the shortfall grows. Set surge_max to 1 to switch it off. All three are in Console → Settings or PATCH /v1/settings.

Customer tracking

Every job has a tracking_url. Send it to the customer by SMS or WhatsApp: it opens a page with the delivery’s status, the rider on a map and the time to arrival. It needs no sign-in, and never shows phone numbers or amounts.

To build tracking into your own site or app, call GET /v1/track/{token} with the token at the end of that link. It is public and may be called from a browser on any site.

API reference

All 35 operations, generated from the OpenAPI description this server publishes.

Jobs

Deliveries: create them, follow them, change them.

POST /v1/jobs

Create a job

Creates a delivery and starts looking for a rider straight away.

Authentication: API key

Parameters
NameInNotes
Idempotency-Keyheaderstring. Any unique string per order. Retrying with the same key never creates a second delivery.
Request body

NewJob

Responses
200This Idempotency-Key was already used; the original job is returned. · Job
201Created · Job
400Error · Error
401Error · Error
429Error · Error

GET /v1/jobs

List jobs

Authentication: API key

Parameters
NameInNotes
statequeryJobState. Only jobs in this state.
limitqueryinteger. 1–200, default 50.
beforequerystring (uuid). Pass next_before from the previous page.
Responses
200OK · object
FieldTypeNotes
jobsarray of Job
next_beforestring (uuid) or null
400Error · Error
401Error · Error

GET /v1/jobs/{id}

Get a job

Authentication: API key

Parameters
NameInNotes
id requiredpathstring. Job id
Responses
200OK · Job
401Error · Error
404Error · Error

GET /v1/jobs/{id}/events

Get a job's history

Authentication: API key

Parameters
NameInNotes
id requiredpathstring. Job id
Responses
200OK · object
FieldTypeNotes
eventsarray of object
FieldTypeNotes
idinteger
typestring
created_atinteger
401Error · Error
404Error · Error

GET /v1/jobs/{id}/route

Get the route still to travel

Authentication: API key

Parameters
NameInNotes
id requiredpathstring. Job id
Responses
200OK · object
FieldTypeNotes
pointsarray of object
FieldTypeNotes
latnumber
lngnumber
duration_snumber
distance_mnumber
sourceroad | straight
401Error · Error
404Error · Error

POST /v1/jobs/{id}/cancel

Cancel a job

Allowed until the rider has picked the goods up.

Authentication: API key

Parameters
NameInNotes
id requiredpathstring. Job id
Responses
200OK · Job
401Error · Error
404Error · Error
409Error · Error

POST /v1/jobs/{id}/assign

Assign a job to a rider

Skips the offer and gives a waiting job to a named rider.

Authentication: API key

Parameters
NameInNotes
id requiredpathstring. Job id
Request body

object

FieldTypeNotes
worker_id requiredstring (uuid)
Responses
200OK · Job
401Error · Error
404Error · Error
409Error · Error

POST /v1/jobs/{id}/unassign

Put a job back in the queue

Authentication: API key

Parameters
NameInNotes
id requiredpathstring. Job id
Responses
200OK · Job
401Error · Error
404Error · Error
409Error · Error

POST /v1/quotes

Quote a delivery

Fee, distance and pickup time before the order is placed. Nothing is created.

Authentication: API key

Request body

object

FieldTypeNotes
pickup requiredobject
FieldTypeNotes
lat requirednumber
lng requirednumber
dropoff requiredobject
FieldTypeNotes
lat requirednumber
lng requirednumber
Responses
200OK · Quote
400Error · Error
401Error · Error

Riders

The people who deliver.

POST /v1/workers

Add a rider

Authentication: API key

Request body

NewWorker

Responses
201Created · object
FieldTypeNotes
workerWorker
tokenstringThe rider's token. Shown once.
400Error · Error
401Error · Error
409Error · Error

GET /v1/workers

List riders

Authentication: API key

Responses
200OK · object
FieldTypeNotes
workersarray of Worker
401Error · Error

GET /v1/workers/{id}

Get a rider

Authentication: API key

Parameters
NameInNotes
id requiredpathstring. Rider id
Responses
200OK · Worker
401Error · Error
404Error · Error

DELETE /v1/workers/{id}

Remove a rider

Refused while the rider has jobs in hand or cash outstanding.

Authentication: API key

Parameters
NameInNotes
id requiredpathstring. Rider id
Responses
204Done, no content.
401Error · Error
404Error · Error
409Error · Error

POST /v1/workers/{id}/token

Replace a rider's token

Authentication: API key

Parameters
NameInNotes
id requiredpathstring. Rider id
Responses
200OK · object
FieldTypeNotes
tokenstring
401Error · Error
404Error · Error

POST /v1/workers/{id}/settle

Record cash handed in

Authentication: API key

Parameters
NameInNotes
id requiredpathstring. Rider id
Request body

object

FieldTypeNotes
amount requiredintegerMoney is always an integer in minor units (cents).
notestring
Responses
200OK · object
FieldTypeNotes
cash_on_handinteger
400Error · Error
401Error · Error
404Error · Error
409Error · Error

Webhooks

Be told when something happens instead of asking.

PUT /v1/webhook

Set the webhook address

Authentication: API key

Request body

object

FieldTypeNotes
url requiredstringMust be https and publicly reachable.
Responses
200OK · object
FieldTypeNotes
urlstring
secretstringSigning secret. Shown once; setting the address again issues a new one.
400Error · Error
401Error · Error
403Error · Error

DELETE /v1/webhook

Remove the webhook

Authentication: API key

Responses
204Done, no content.
401Error · Error
403Error · Error

GET /v1/webhook/deliveries

Recent deliveries

Authentication: API key

Responses
200OK · object
FieldTypeNotes
urlstring or null
deliveriesarray of object
FieldTypeNotes
idinteger
job_idstring (uuid)
typestring
statedelivered | retrying | queued | gave_up
attemptsinteger
statusinteger or null
errorstring or null
created_atinteger
delivered_atinteger or null
401Error · Error
403Error · Error

POST /v1/webhook/test

Send a test event

Sends a signed ping event now and reports your server's answer.

Authentication: API key

Responses
200OK · object
FieldTypeNotes
okboolean
statusinteger or null
errorstring
msinteger
401Error · Error
403Error · Error
409Error · Error

Account

Dispatch and pricing settings.

GET /v1/settings

Get settings

Authentication: API key

Responses
200OK · Settings
401Error · Error

PATCH /v1/settings

Change settings

Authentication: API key

Request body

object

FieldTypeNotes
offer_timeout_sintegerSeconds a rider has to answer an offer (5–600).
search_radius_mintegerHow far from the pickup to look for riders (200–50000).
hold_for_prepbooleanHold a job back until a rider would arrive about when the goods are ready.
fee_baseintegerMoney is always an integer in minor units (cents).
fee_per_kmintegerMoney is always an integer in minor units (cents).
surge_maxnumberHighest busy-area multiplier, 1–5. 1 switches it off.
Responses
200OK · Settings
400Error · Error
401Error · Error
403Error · Error

Insight

Numbers for dashboards.

GET /v1/heatmap

Riders and waiting orders by hexagon

Authentication: API key

Responses
200OK · object
FieldTypeNotes
cellsarray of object
FieldTypeNotes
cellstringH3 index, resolution 8.
centreobject
boundaryarray of object
ridersinteger
open_jobsinteger
401Error · Error

GET /v1/stats

Counts and hourly totals

Authentication: API key

Parameters
NameInNotes
hoursqueryinteger. 1–168, default 24.
Responses
200OK · object
FieldTypeNotes
hoursinteger
totalsobject
hourlyarray of object
401Error · Error

Tracking

What your customer sees.

GET /v1/track/{token}

Track a delivery

Public: the token is the credential. Callable from any website. Never returns phone numbers or amounts.

Authentication: None (public)

Parameters
NameInNotes
token requiredpathstring. The token at the end of a job's tracking_url.
Responses
200OK · Tracking
404Error · Error

GET /healthz

Service health

Authentication: None (public)

Responses
200OK · object
FieldTypeNotes
postgresboolean
redisboolean

Rider app

Endpoints a rider's app calls with the rider's own token.

GET /v1/worker/me

The rider's profile

Authentication: Rider token

Responses
200OK · Worker
401Error · Error

POST /v1/worker/duty

Go on or off duty

Authentication: Rider token

Request body

object

FieldTypeNotes
on_duty requiredboolean
Responses
200OK · Worker
401Error · Error

POST /v1/worker/locations

Upload positions

Send 1–200 fixes; buffer them when offline. The newest keeps the rider visible to dispatch, and being within 60 m of the next stop marks arrival.

Authentication: Rider token

Request body

object

FieldTypeNotes
points requiredarray of object
FieldTypeNotes
lat requirednumber
lng requirednumber
atstring (date-time)
accuracy_mnumber
Responses
200OK · object
FieldTypeNotes
acceptedinteger
liveboolean
arrivedarray of string (uuid)Jobs marked as arrived by this upload.
400Error · Error
401Error · Error
429Error · Error

GET /v1/worker/offers

The offer waiting on the rider

Authentication: Rider token

Responses
200OK · object
FieldTypeNotes
offersarray of Offer
401Error · Error

POST /v1/worker/offers/{id}/accept

Accept an offer

Authentication: Rider token

Parameters
NameInNotes
id requiredpathstring. Offer id
Responses
200OK · Job
401Error · Error
404Error · Error
409Error · Error

POST /v1/worker/offers/{id}/decline

Decline an offer

Authentication: Rider token

Parameters
NameInNotes
id requiredpathstring. Offer id
Responses
200OK · object
FieldTypeNotes
declinedboolean
401Error · Error
409Error · Error

GET /v1/worker/jobs

Jobs in hand

Authentication: Rider token

Responses
200OK · object
FieldTypeNotes
jobsarray of Job
401Error · Error

GET /v1/worker/plan

Stops to visit, in order

Authentication: Rider token

Responses
200OK · object
FieldTypeNotes
stopsarray of PlanStop
length_minteger
401Error · Error

POST /v1/worker/jobs/{id}/progress

Report progress on a job

Authentication: Rider token

Parameters
NameInNotes
id requiredpathstring. Job id
Request body

Progress

Responses
200OK · Job
400Error · Error
401Error · Error
404Error · Error
409Error · Error

POST /v1/worker/jobs/{id}/release

Give a job back

Allowed until the goods are picked up.

Authentication: Rider token

Parameters
NameInNotes
id requiredpathstring. Job id
Responses
200OK · Job
401Error · Error
404Error · Error
409Error · Error

Objects

Error

FieldTypeNotes
error requiredobject
FieldTypeNotes
code requiredstringStable, machine-readable reason, e.g. invalid_transition.
message requiredstringHuman-readable explanation.

StopInput

FieldTypeNotes
lat requirednumberLatitude, -90 to 90.
lng requirednumberLongitude, -180 to 180.
addressstring
namestringBusiness or person at this stop.
phonestring

Stop

FieldTypeNotes
lat requirednumber
lng requirednumber
address requiredstring
name requiredstring
phone requiredstring

JobState

searching → assigned → at_pickup → picked_up → at_dropoff → delivered. failed and cancelled are the other ways a job ends.

searching | assigned | at_pickup | picked_up | at_dropoff | delivered | failed | cancelled

Job

FieldTypeNotes
id requiredstring (uuid)
external_refstring or nullYour own reference, e.g. an order number.
state requiredJobState
pickup requiredStop
dropoff requiredStop
ready_atstring (date-time) or nullWhen the goods will be ready to collect.
cash_to_collect requiredintegerCash the rider must collect at the door. Money is always an integer in minor units (cents).
cash_collectedinteger or null
notesstring
proofobject or null
failure_reasonstring or null
worker_idstring (uuid) or null
tracking_url requiredstringLink to give the customer. Shows live status without signing in.
created_at requiredstring (date-time)
assigned_atstring (date-time) or null
picked_up_atstring (date-time) or null
completed_atstring (date-time) or null

NewJob

FieldTypeNotes
pickup requiredStopInput
dropoff requiredStopInput
external_refstring
ready_atstring (date-time)
cash_to_collectintegerDefaults to 0 (prepaid). Money is always an integer in minor units (cents).
notesstring

Worker

FieldTypeNotes
id requiredstring (uuid)
name requiredstring
phone requiredstring
vehicle requiredbicycle | motorbike | car | van | truck | foot
capacity requiredintegerJobs the rider can carry at once.
cash_limitinteger or nullThe most cash the rider may hold; null for no limit.
cash_on_hand requiredinteger
on_duty requiredboolean
online requiredbooleanOn duty and heard from recently enough to be offered work.
active_jobs requiredinteger
last_latnumber or null
last_lngnumber or null
last_seen_atstring (date-time) or null

NewWorker

FieldTypeNotes
name requiredstring
phone requiredstring5 to 20 digits, optionally starting with +. Unique per account.
vehiclestring
capacityinteger
cash_limitinteger or null

Settings

FieldTypeNotes
idstring (uuid)
namestring
offer_timeout_sintegerSeconds a rider has to answer an offer (5–600).
search_radius_mintegerHow far from the pickup to look for riders (200–50000).
webhook_urlstring or null
hold_for_prepbooleanHold a job back until a rider would arrive about when the goods are ready.
fee_baseintegerMoney is always an integer in minor units (cents).
fee_per_kmintegerMoney is always an integer in minor units (cents).
surge_maxnumberHighest busy-area multiplier, 1–5. 1 switches it off.
eta_factornumberLearned: real travel time divided by the estimate. Read-only.
eta_samplesinteger

Quote

FieldTypeNotes
feeintegerMoney is always an integer in minor units (cents).
multipliernumber1.0 normally; higher when the area has more orders than free riders.
distance_minteger
trip_sintegerEstimated travel time from pickup to drop-off.
pickup_eta_sinteger or nullHow soon a free rider could reach the pickup; null if none is in range.
riders_in_rangeinteger
free_riders_nearbyinteger
orders_waiting_nearbyinteger

Offer

FieldTypeNotes
idstring (uuid)
expires_atintegerUnix seconds.
seconds_leftinteger
eta_to_pickup_sinteger
trip_distance_minteger
jobobjectContact details are held back until the rider accepts.
FieldTypeNotes
idstring (uuid)
pickupobject
dropoffobject
cash_to_collectinteger
ready_atinteger or null

Progress

What the rider reports. Arrivals are also detected automatically from the rider's position.

Shape 1

FieldTypeNotes
type required"arrived_pickup"

Shape 2

FieldTypeNotes
type required"picked_up"

Shape 3

FieldTypeNotes
type required"arrived_dropoff"

Shape 4

FieldTypeNotes
type required"delivered"
proofobject
FieldTypeNotes
photo_urlstring
signature_urlstring
recipient_namestring
notesstring
cash_collectedintegerDefaults to the amount the job asked for.

Shape 5

FieldTypeNotes
type required"failed"
reason requiredstring

PlanStop

FieldTypeNotes
job_idstring (uuid)
kindpickup | dropoff
latnumber
lngnumber
namestring
addressstring
phonestring
cash_to_collectinteger
external_refstring or null

Tracking

FieldTypeNotes
stateJobState
pickupobject
dropoffobject
riderobject or null
eta_sinteger or null
routearray of object or null
created_atinteger
picked_up_atinteger or null
completed_atinteger or null

WebhookEvent

The body POSTed to your webhook address.

FieldTypeNotes
idintegerIncreases with every event. De-duplicate and order on this.
typestringe.g. job.delivered.
job_idstring (uuid)
created_atintegerUnix seconds.
dataobject
FieldTypeNotes
jobJob