SwiftGo Business API

Book SwiftGo riders from your online shop or storefront platform. Get a price, book a pickup from your seller and a drop-off to your buyer, and receive live status updates on your server.

https://api.swiftgogh.com/api/business/v1 v1

Quick start

Five minutes with a test key. Sign up at developer.swiftgogh.com and you get one straight away. Test mode books pretend deliveries: no riders, no money. A simulator moves each one through every status and sends you real webhooks.

1 · Check your key
curl https://api.swiftgogh.com/api/business/v1/ping \
  -H "Authorization: Bearer sk_test_..."
# 2 · Book a delivery
curl -X POST https://api.swiftgogh.com/api/business/v1/deliveries \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: order-1001" \
  -H "Content-Type: application/json" \
  -d '{
    "external_reference": "1001",
    "pickup":  { "latitude": 5.5570, "longitude": -0.1830,
                 "address": "12 Oxford St, Osu",
                 "contact": { "name": "Ama Shoes", "phone": "0241234567" } },
    "dropoff": { "latitude": 5.6395, "longitude": -0.1530,
                 "address": "East Legon", "landmark": "Near A&C Mall",
                 "contact": { "name": "Kofi Mensah", "phone": "0551234567" },
                 "instructions": "Call on arrival" },
    "package": { "description": "Shoes (1 box)", "size": "small", "value": 250 }
  }'

You get back a delivery with "status": "pending". In test mode it moves to rider_assigned, picked_up, arriving and delivered over about two minutes. To jump ahead, call POST /deliveries/{id}/simulate with {"status": "delivered"} or "failed".

Keys and authentication

sk_test_…

Pretend deliveries for building and testing. Created for you when you sign up at developer.swiftgogh.com.

sk_live_…

Real riders and real deliveries, billed to your account. Request live access in the developer portal; you can create live keys once SwiftGo approves it and sets your credit limit.

Send your key on every request as Authorization: Bearer sk_live_… (or X-Api-Key: sk_live_…). Test and live data are completely separate: a test key can't see live deliveries and the other way round.

Keep keys on your server. Never put them in a browser, a mobile app or a code repository. If a key leaks, ask SwiftGo to revoke it and issue a new one.
  • Every response has an X-Request-Id header and every error a request_id. Include it when you contact SwiftGo.
  • Rate limit: 120 requests per minute per key. Over the limit you get 429 RATE_LIMITED with a Retry-After header in seconds.
  • JSON in, JSON out. Times are ISO-8601 in UTC. Money is cedis (GHS) as a number with 2 decimals.

Endpoints

Account
GET/pingCheck a key works, and see which account and mode it belongs to.
GET/coverage?latitude=&longitude=Whether SwiftGo delivers at a point. Without coordinates, lists the delivery areas.
Quotes
POST/quotesPrice a pickup and drop-off. The price is held for 15 minutes.
GET/quotes/{id}Read a quote back.
Deliveries
POST/deliveriesBook a delivery. Needs an Idempotency-Key header.
GET/deliveriesList deliveries, newest first. Filter by status, external_reference, created_after, created_before. Page with limit (up to 100) and starting_after.
GET/deliveries/{id}One delivery, with its current status, rider and tracking link.
POST/deliveries/{id}/cancelCancel. Free until a rider accepts; not possible after pickup.
POST/deliveries/{id}/simulateTest mode only: move a delivery to rider_assigned, picked_up, arriving, delivered or failed now.
Webhooks
GET/webhook-endpointWhere status updates are sent for this mode.
PUT/webhook-endpointSet the URL (https). The signing secret is returned only the first time.
POST/webhook-endpoint/rotate-secretIssue a new signing secret, shown once.
POST/webhook-endpoint/testSend a signed ping now (up to 5 per minute).
GET/eventsRecent events and whether each reached you. Filter by delivery_id or type.
POST/events/{id}/resendSend an event again.

IDs carry a prefix that tells you what they are: del_ deliveries, qt_ quotes, evt_ events.

Quotes

Optional. Get a price before you book, for example to show the delivery fee at your checkout.

POST /quotes
{ "pickup":  { "latitude": 5.5570, "longitude": -0.1830 },
  "dropoff": { "latitude": 5.6395, "longitude": -0.1530 } }
201 Created
{
  "id": "qt_01jcx7k8f9m2r3t4v5w6x7y8z9",
  "object": "quote",
  "livemode": false,
  "price": { "amount": 31.5, "delivery_fee": 26.5, "service_fee": 5.0, "currency": "GHS" },
  "distance_km": 9.8,
  "eta_minutes": 38,
  "zone": { "name": "Accra Central" },
  "expires_at": "2026-11-12T10:15:00Z",
  "used": false
}

Pass the quote's id as quote_id when you book to be billed exactly that price. A quote works once, for the same mode, and only for (nearly) the same pickup and drop-off: within about 150 metres.

Booking a delivery

POST /deliveries with an Idempotency-Key header. Phone numbers can be in any common Ghana format (0241234567, +233241234567, 233 24 123 4567) and come back as +233….

FieldRequiredNotes
pickup.latitude, pickup.longitudeyesWhere the rider collects. Ghana only.
pickup.contact.name, .phoneyesWho hands the package over. The rider can call them.
dropoff.latitude, dropoff.longitudeyesWhere the rider delivers.
dropoff.contact.name, .phoneyesThe recipient. They get an SMS with a live tracking link and the delivery code.
package.descriptionyesWhat's being delivered, e.g. "Shoes (1 box)". 200 characters max.
*.address, *.landmark, *.ghana_post_gps, *.instructionsoptionalShown to the rider for pickup and drop-off. GhanaPostGPS like GA-123-4567.
package.sizeoptionalsmall, medium, large or xlarge.
package.weight_kgoptionalUp to 30 (motorbike delivery).
package.value, quantity, fragileoptionalDeclared value in GHS, 1 to 50 items, true/false.
quote_idoptionalBill the quoted price.
external_referenceoptionalYour order ID. You can search by it.
merchant.external_id, .name, .phoneoptionalPlatforms: the seller on your platform. The rider and the recipient see the name.
metadataoptionalUp to 20 of your own key/value pairs (keys up to 40 characters, values up to 500), returned unchanged.

The delivery object

GET /deliveries/del_…
{
  "id": "del_01jcx9p2q3r4s5t6v7w8x9y0z1",
  "object": "delivery",
  "livemode": true,
  "status": "rider_assigned",
  "external_reference": "1001",
  "order_number": "SG-20261112-04512",
  "price": { "amount": 31.5, "delivery_fee": 26.5, "service_fee": 5.0, "currency": "GHS" },
  "distance_km": 9.8,
  "pickup":  { "latitude": 5.557, "longitude": -0.183, "address": "12 Oxford St, Osu",
               "contact": { "name": "Ama Shoes", "phone": "+233241234567" } },
  "dropoff": { "latitude": 5.6395, "longitude": -0.153, "address": "East Legon",
               "contact": { "name": "Kofi Mensah", "phone": "+233551234567" } },
  "package": { "description": "Shoes (1 box)", "size": "small", "value": 250, "quantity": 1, "fragile": false },
  "rider": { "name": "Yaw M.", "phone": "+233...", "photo_url": "...",
             "vehicle_type": "motorbike", "vehicle_plate": "GT 1234-24" },
  "tracking_url": "https://swiftgogh.com/t/...",
  "eta": { "dropoff_at": "2026-11-12T10:41:00Z" },
  "proof_of_delivery": null,
  "cancellation": null,
  "timeline": [ { "status": "pending", "at": "2026-11-12T10:02:11Z" },
                { "status": "rider_assigned", "at": "2026-11-12T10:04:37Z" } ],
  "metadata": { "platform_order": "1001" },
  "created_at": "2026-11-12T10:02:11Z",
  "updated_at": "2026-11-12T10:04:37Z"
}
  • rider.phone is shown only while the delivery is in progress, so the seller can hand over the package.
  • tracking_url is a public live-tracking page for the recipient. It is null in test mode and stops working a day after the delivery ends.
  • proof_of_delivery appears once delivered: otp_verified (the recipient gave the rider their code) and photo_url if the rider took a photo.

Statuses

StatusWhat it means
pendingBooked. SwiftGo is finding a rider.
rider_assignedA rider accepted and is heading to the pickup. Sent again if a different rider takes over.
picked_upThe rider has the package. The recipient gets the tracking-link SMS.
arrivingThe rider is close. The recipient gets an SMS with the delivery code.
deliveredDone. See proof_of_delivery.
failedThe rider couldn't complete it, for example the recipient couldn't be reached.
cancelledCancelled by you or by SwiftGo, for example no rider was available. See cancellation.cancelled_by.

A status only moves forward, and delivered, failed and cancelled are final. One exception: if the rider drops the job before pickup, the delivery goes back to pending (event delivery.rider_unassigned, rider details removed) until another rider accepts.

Safe retries

Every POST /deliveries needs an Idempotency-Key header: any unique string, such as your order ID. If a request times out, send it again with the same key and the same body.

  • If the first request worked, you get the same delivery back with Idempotent-Replayed: true. A second delivery is never created.
  • The same key with a different body is refused with 422 IDEMPOTENCY_KEY_REUSED.
  • If the first request is still running, you get 409 IDEMPOTENCY_IN_PROGRESS. Retry after a second or two.
  • Keys are remembered for 24 hours. POST /deliveries/{id}/cancel accepts one too.

Cancelling and charges

WhenCan you cancel?Charge
pending, no rider yetYesFree
rider_assignedYesCancellation fee, 50% of the price by default
picked_up or arrivingNo. Contact SwiftGo to arrange a return.
Cancelled by SwiftGoFree
failedFailed-attempt fee, 100% of the price by default
deliveredThe delivery price

Your contract can set different percentages. Live deliveries are billed to your account and invoiced at the start of each month; each delivery is billed once. A new live delivery needs room under your credit limit, counting deliveries still in progress (402 CREDIT_LIMIT_EXCEEDED otherwise). An overdue invoice pauses new live bookings (402 ACCOUNT_PAST_DUE) until it's paid.

Webhooks

Tell SwiftGo where to send status updates. Set one https URL per mode.

PUT /webhook-endpoint
curl -X PUT https://api.swiftgogh.com/api/business/v1/webhook-endpoint \
  -H "Authorization: Bearer sk_test_..." -H "Content-Type: application/json" \
  -d '{"url": "https://yourshop.com/swiftgo/webhooks"}'
Store the secret (whsec_…) from the first response. It isn't shown again. POST /webhook-endpoint/rotate-secret issues a new one.
What SwiftGo sends
POST https://yourshop.com/swiftgo/webhooks
SwiftGo-Signature: t=1762942951,v1=5f2c…
SwiftGo-Event-Type: delivery.picked_up
SwiftGo-Event-Id: evt_01jcxa…

{ "id": "evt_01jcxa…", "object": "event", "type": "delivery.picked_up", "livemode": true,
  "created_at": "2026-11-12T10:22:31Z",
  "data": { "object": { …the delivery object… } } }

Event types: delivery.created, delivery.rider_assigned, delivery.rider_unassigned, delivery.picked_up, delivery.arriving, delivery.delivered, delivery.failed, delivery.cancelled.

  • Answer with any 2xx within 8 seconds. Otherwise SwiftGo retries after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours.
  • After 100 failures in a row the endpoint is switched off. Saving it again turns it back on.
  • An event can arrive twice or out of order. Use its id to ignore repeats, and the delivery's status and updated_at to keep the latest.
  • GET /events shows recent events and whether each reached you; POST /events/{id}/resend sends one again.

Check the signature

v1 is the HMAC-SHA256 of "<t>.<raw request body>" with your secret. Reject the request if it doesn't match, or if t is more than 5 minutes old.

const crypto = require('crypto');

app.post('/swiftgo/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('SwiftGo-Signature') || '';
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const expected = crypto.createHmac('sha256', process.env.SWIFTGO_WEBHOOK_SECRET)
    .update(`${parts.t}.${req.body}`).digest('hex');
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  if (!fresh || !parts.v1 || parts.v1.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);
  // update your order using event.data.object.status
  res.sendStatus(200);
});

Errors

Every error has this shape
{ "error": { "type": "invalid_request_error",
             "code": "QUOTE_EXPIRED",
             "message": "This quote has expired. Get a new quote.",
             "errors": { "quote_id": ["This quote has expired. Get a new quote."] } },
  "request_id": "req_01jcxb…" }

Branch on code, which never changes. message is written for people and may change.

HTTPCodesWhat to do
400IDEMPOTENCY_KEY_REQUIREDAdd an Idempotency-Key header.
401AUTH_MISSING_KEY, AUTH_INVALID_KEY, AUTH_KEY_REVOKED, ACCOUNT_CLOSEDCheck the key you're sending.
402CREDIT_LIMIT_NOT_SET, CREDIT_LIMIT_EXCEEDED, ACCOUNT_PAST_DUEBilling. Contact SwiftGo.
403ACCOUNT_NOT_ACTIVE, ACCOUNT_SUSPENDED, TEST_MODE_ONLYYour account isn't approved for this.
404NOT_FOUNDWrong ID, or it belongs to the other mode.
409QUOTE_ALREADY_USED, DELIVERY_IN_TRANSIT, DELIVERY_FINISHED, INVALID_TRANSITION, IDEMPOTENCY_IN_PROGRESS, BUSYThe delivery or quote is in the wrong state for this.
422VALIDATION_FAILED (see errors), OUT_OF_COVERAGE, TOO_FAR, QUOTE_NOT_FOUND, QUOTE_EXPIRED, QUOTE_MISMATCH, IDEMPOTENCY_KEY_REUSED, INVALID_WEBHOOK_URLFix the request.
429RATE_LIMITEDWait Retry-After seconds.
500INTERNAL_ERROROur fault. Retry with the same Idempotency-Key.
503LIVE_MODE_UNAVAILABLELive bookings are paused. Test mode still works.

Going live

  1. Build and test everything with your sk_test_ key, including webhooks and the signature check.
  2. Request live access in the developer portal (Overview → Go live). SwiftGo reviews it and sets your credit limit.
  3. Create a live key in the portal, set your live webhook URL, then send a test event (portal or POST /webhook-endpoint/test).
  4. Book one real delivery between two addresses you control.
  5. Switch your shop to the live key.