Gozem Developer DocsDocs

This guide takes you through a complete delivery, the way a real integration runs it: pick a vehicle, price the delivery, book it, follow it to completion through webhooks, then pull the invoice. It also covers how to exercise the whole flow in sandbox, where there are no live drivers.

The examples use the sandbox host; for production, swap sandbox-api.gozem.co for api.gozem.co.

Prerequisites

  • A client_id and client_secret from the Partner Portal (see Environments & Access)
  • A REST client such as curl or Postman
  • Basic familiarity with HTTP APIs

The shape of a courier integration

A delivery has three phases. You prepare it by choosing a vehicle and, usually, creating a quote. You book it, which creates a trip and starts the search for a driver. You then follow it as the driver picks up and delivers, and finally read the invoice. The first two phases are synchronous request-and-response. The third is asynchronous: the trip changes state over time, and the clean way to learn about those changes is through webhooks.

Step 1: Get an access token

Authenticate with your credentials to get a bearer token, then send it as Authorization: Bearer <token> on every call below. The full flow, token caching, and the API-key alternative are covered in Authentication.

curl -X POST https://sandbox-auth.gozem.co/oauth2/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "client_credentials",
    "client_id": "CLT_a1b2c3d4e5",
    "client_secret": "YOUR_CLIENT_SECRET"
  }'

Step 2: Choose a vehicle

Call GET /courier/v1/vehicles with the pickup location to see which vehicle types serve that area. Pass a location, either coordinates or a city:

curl "https://sandbox-api.gozem.co/courier/v1/vehicles?coordinates=6.1725,1.2314" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{
  "success": true,
  "message": "Ok",
  "data": [
    {
      "guid": "VEH_XRK6EBcaq0",
      "name": "Motorcycle",
      "description": "Fast delivery for small packages",
      "city_coverage": [
        {
          "city_name": "Lomé",
          "country_name": "Togo",
          "country_code": "TG",
          "geo_polygon": {
            "type": "Polygon",
            "coordinates": [...]
          }
        }
      ]
    }
  ]
}

Each result has a guid (prefix VEH_) and a name such as Motorcycle, Car, or Van. Pass the guid (here, VEH_XRK6EBcaq0) as vehicle in the next step. If the vehicle you want is not in the response, it does not operate in that area, and a quote or booking for it will fail. See Get available vehicles for the full parameters and response.

Step 3: Create a quote

A quote prices a specific pickup and set of dropoffs for a chosen vehicle, and it is the value you show the customer before they commit. Post the pickup and dropoffs to POST /courier/v1/quotes:

curl -X POST https://sandbox-api.gozem.co/courier/v1/quotes \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "vehicle": "VEH_XRK6EBcaq0",
    "pickup": {
      "lat": 6.1725,
      "lon": 1.2314,
      "label": "Gozem Hub, Boulevard du Mono, Lomé",
      "contact_name": "John Doe",
      "contact_phone": "+228XXXXXXXX"
    },
    "dropoffs": [
      {
        "lat": 6.1804,
        "lon": 1.2456,
        "label": "Pharmacie du Port, Lomé",
        "contact_name": "Jane Doe",
        "contact_phone": "+228XXXXXXXX"
      }
    ]
  }'
{
  "success": true,
  "message": "Ok",
  "data": {
    "guid": "TQT_7h2k9p4m1n",
    "estimated_distance": 5.2,
    "estimated_duration": 15,
    "estimated_fare": {
      "amount": 2500,
      "currency_code": "XOF",
      "breakdown": [
        {
          "label": "Pharmacie du Port, Lomé",
          "amount": 2500,
          "sub_label": "Delivery 1"
        }
      ]
    },
    "expires_at": "2026-01-15T10:30:00.000Z",
    "created_at": "2026-01-15T10:15:00.000Z"
  }
}

Two things matter here. A quote is time-limited, so book it before expires_at or create a fresh one. And for a multi-dropoff run, set optimize_route to true to let the server order the stops for you; the dropoffs come back in delivery order.

Quoting is optional. If you do not need to show a price first, skip to booking and send the full pickup and dropoff payload directly. See Create a quote for all quote fields.

Step 4: Book the trip

Book against the trips endpoint, POST /courier/v1/trips. To book from a quote, send its identifier:

curl -X POST https://sandbox-api.gozem.co/courier/v1/trips \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "quote_id": "TQT_7h2k9p4m1n" }'
{
  "success": true,
  "message": "Ok",
  "data": {
    "guid": "TRP_48273910",
    "status": "pending",
    "tracking_url": "https://track.gozem.co/t/eyJhbGci...",
    "fare": {
      "amount": 2500,
      "currency_code": "XOF",
      "breakdown": [
        {
          "label": "Pharmacie du Port, Lomé",
          "amount": 2500,
          "sub_label": "Delivery 1"
        }
      ]
    },
    "created_at": "2026-01-15T10:16:00.000Z",
    "updated_at": "2026-01-15T10:16:00.000Z"
  }
}

To book without a quote, send the same pickup and dropoff payload you would send to /quotes. Either way you get back a trip with a guid (prefix TRP_), a status of pending, and a tracking_url you can share with the sender or recipient. Store the trip guid; every later call uses it.

Booking charges the merchant’s Gozem wallet. If the wallet balance is too low, the booking is rejected with insufficient_wallet_balance, so keep it funded for your expected volume.

A trip starts at pending while the platform looks for a driver. From here you stop driving the flow with requests and start reacting to events. See Book a trip for the full booking options.

Step 5: Follow the trip with webhooks

Configure a webhook in the Partner Portal, subscribe to the Courier trip events, and handle the POSTs Gozem sends as the delivery progresses. This is better than polling GET /courier/v1/trips/:trip_id on a timer, because you find out the moment something changes and you make far fewer calls.

A typical successful delivery fires this sequence:

courier.trip.created    → trip is in the system
courier.trip.assigned   → a driver is available and heading to pickup
courier.trip.at_pickup  → driver reached the pickup
courier.trip.started    → package collected, delivery underway
courier.trip.completed  → all dropoffs delivered

On a trip with more than one dropoff, each stop also fires courier.trip.stop.started and courier.trip.stop.completed as the driver works through the dropoffs, between started and completed.

Verify every event’s signature, respond 2xx quickly, and process the work in the background; events can be retried, so handle them idempotently using the event guid. The delivery mechanism, signature scheme, and retry behavior are covered in the platform Webhooks guide; the complete list of Courier events, statuses, and transitions is in Trip Lifecycle & Events.

You can always read the current state directly with GET /courier/v1/trips/:trip_id as a fallback or for reconciliation.

Step 6: Completion and invoice

When the trip reaches completed, an invoice is available at GET /courier/v1/trips/:trip_id/invoice. It carries the invoice number, the final fare breakdown, and the payment method and status. Invoices exist only for completed trips; a canceled or unfinished trip has none.

If you collect feedback, submit a rating with POST /courier/v1/trips/:trip_id/rating once the trip is completed.

Handling cancellation

A trip can be canceled while it is still in progress (pending, assigned, or at_pickup) with POST /courier/v1/trips/:trip_id/cancellation. Once a trip is completed, canceled, or expired, it is terminal and cannot change; a cancel call against a terminal trip returns 409 Conflict. A trip that is never accepted moves to expired on its own. Both cancellation and expiry fire their own events (courier.trip.canceled, courier.trip.expired), so your webhook handler should treat them as normal endings, not errors.

Testing the full lifecycle in sandbox

Everything through Step 4 works in sandbox exactly as in production. Step 5 is where sandbox differs: there are no live drivers, so a booked trip stays at pending and none of the assigned, at_pickup, started, or completed events fire on their own.

To test tracking and webhooks end to end, use the sandbox testing tools provided with your sandbox access. They let you move a sandbox trip through its lifecycle, which generates the same events your webhook handler will see in production. Book a trip, point your webhook at a test endpoint, then drive the trip forward and confirm each event arrives and is handled correctly.

Before you go to production

Confirm your integration handles the unhappy paths as well as the clean delivery: expired quotes, a vehicle that does not serve the area, an insufficient wallet balance, cancellations, expiry, rate limiting, and retried webhooks. Verify your webhook signature check and your idempotency on the event guid. Then recreate your client and credentials in the production portal, switch your base URL to https://api.gozem.co, and run one real low-value delivery to confirm the live path before opening the integration up.