Why this page exists (read this first)
GET /capabilities is not a “nice to have.” It is the read-only mirror of the business dashboard for one org.
| Layer | Role |
|---|---|
| Dashboard → Settings → Delivery | Owner turns services, payments, POD, coverage, auto-dispatch on/off |
GET /capabilities |
Your backend learns those same rules (already clamped by plan + trust) |
| Your bot / form | Only asks for fields that are allowed for this org |
POST /deliveries |
Rejects jobs that ignore those rules (service_disabled, bad payment method, etc.) |
The public request form already does this: it loads delivery settings, then only shows enabled service kinds, warehouse mode, errand modes, payment options, and saved places. Your API integration should behave the same way.
If you skip discovery, bots invent services the fleet never enabled, ask for cash when only online checkout is on, or guess hostel coordinates — create fails or riders get bad pins.
Mental picture (dashboard → API → chat)
┌─────────────────────────────────────────────────────────────┐
│ Kilo Business dashboard │
│ Settings → Delivery │
│ • Pickup & deliver / Errand toggles │
│ • Warehouse drop-off │
│ • Errand modes (at place / link / photo) │
│ • Payment methods + when customer pays │
│ • POD, coverage, auto-dispatch, pricing │
│ Settings → Warehouses │
│ Settings → Service locations (hostels, gates, …) │
└───────────────────────────┬─────────────────────────────────┘
│ same truth (plan-clamped)
▼
┌─────────────────────────────────────────────────────────────┐
│ GET /capabilities (+ /warehouses + /service-locations) │
│ → enabledServiceKinds, payments, POD, features, … │
└───────────────────────────┬─────────────────────────────────┘
│ drive the conversation
▼
┌─────────────────────────────────────────────────────────────┐
│ Your bot / checkout │
│ only offer what capabilities say is allowed │
│ peep coords from warehouses / service locations │
│ then POST /deliveries │
└─────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────┐
│ Kilo Business dashboard │
│ Settings → Delivery │
│ • Pickup & deliver / Errand toggles │
│ • Warehouse drop-off │
│ • Errand modes (at place / link / photo) │
│ • Payment methods + when customer pays │
│ • POD, coverage, auto-dispatch, pricing │
│ Settings → Warehouses │
│ Settings → Service locations (hostels, gates, …) │
└───────────────────────────┬─────────────────────────────────┘
│ same truth (plan-clamped)
▼
┌─────────────────────────────────────────────────────────────┐
│ GET /capabilities (+ /warehouses + /service-locations) │
│ → enabledServiceKinds, payments, POD, features, … │
└───────────────────────────┬─────────────────────────────────┘
│ drive the conversation
▼
┌─────────────────────────────────────────────────────────────┐
│ Your bot / checkout │
│ only offer what capabilities say is allowed │
│ peep coords from warehouses / service locations │
│ then POST /deliveries │
└─────────────────────────────────────────────────────────────┘Rule for AI agents: treat capabilities as the allow-list. If a value is missing or false, that option is not allowed for this org — do not offer it and do not send it on create.
Discover org settings
curl -X GET 'https://business.kiloapp.org/api/v1/capabilities' \ -H 'Authorization: Bearer sk_live_...'
curl -X GET 'https://business.kiloapp.org/api/v1/capabilities' \
-H 'Authorization: Bearer sk_live_...'const response = await fetch("https://business.kiloapp.org/api/v1/capabilities", {
method: "GET",
headers: {
"Authorization": "Bearer sk_live_..."
},
});
const data = await response.json();
console.log(data);const response = await fetch("https://business.kiloapp.org/api/v1/capabilities", {
method: "GET",
headers: {
"Authorization": "Bearer sk_live_..."
},
});
const data = await response.json();
console.log(data);import requests
url = "https://business.kiloapp.org/api/v1/capabilities"
headers = {
"Authorization": "Bearer sk_live_..."
}
response = requests.request("GET", url, headers=headers)
print(response.json())import requests
url = "https://business.kiloapp.org/api/v1/capabilities"
headers = {
"Authorization": "Bearer sk_live_..."
}
response = requests.request("GET", url, headers=headers)
print(response.json())<?php
$ch = curl_init("https://business.kiloapp.org/api/v1/capabilities");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer sk_live_...",
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;<?php
$ch = curl_init("https://business.kiloapp.org/api/v1/capabilities");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer sk_live_...",
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;Scope: deliveries:read. Cache for the chat/order session; refresh after create errors about service/coverage/payment.
Field → dashboard → allowed / not allowed
Use this table as the contract. Left column = JSON path on capabilities.
| Field | Where the owner sets it | If … | Your bot / create must |
|---|---|---|---|
enabledServiceKinds |
Delivery → service type toggles | kind not in the array | Do not offer that job type; create with that serviceKind → service_disabled |
| only one kind listed | Skip “which service?” — use that kind | ||
| both listed | Ask pickup & deliver vs errand (or infer clearly) | ||
defaultServiceKind |
Delivery → default service | user has not chosen yet | Pre-select this kind |
allowWarehouseDropoff |
Delivery → “Warehouse drop-off” | false |
Do not use deliveryMode: "warehouse" |
true |
May offer warehouse pickup; load GET /warehouses |
||
errand.enabledModes |
Delivery → errand modes | mode missing | Do not collect that errand flow (at_place / link / photo) |
errand.defaultMode |
same | starting an errand | Pre-select this mode |
errand.curatedLinks |
Delivery → curated shop links | link mode | Offer these as suggested shops (name + url) |
errand.allowUnknownShopLocation |
Delivery → unknown shop | false |
Require a known shop pin / curated link — don’t accept vague “any shop” |
paymentCollection.onlineCheckout |
Delivery → Operations / payments | false |
Do not send customerCollectionMethod: "online_checkout" |
paymentCollection.cashOnDelivery |
same | false |
Do not send cash_on_delivery |
| both false (shouldn’t happen after reconcile) | Treat as misconfigured — tell human to fix Delivery settings | ||
currency |
Operations → currency | any | Display money in this code; don’t invent another |
paymentTiming |
Delivery → when customer pays | before_dispatch |
Expect pay-before-rider; rider may wait until fee is paid |
after_delivery |
Job can run; collect fee later | ||
off_platform |
No portal checkout step — fee handled outside Kilo | ||
pod.enabled |
Delivery → Payment on delivery (POD) | false |
Do not enable POD / isPodEnabled on create |
true |
May offer cash-to-collect on pickup_delivery only (never errands) | ||
requireDeliveryOtp |
Delivery → Require OTP at drop-off | true (default) |
Expect a drop-off code (Kilo SMS/WhatsApp or delivery.otp_issued if OTP-via-webhook) |
false |
Photo proof only — no code, no delivery.otp_issued, do not ask the customer for an OTP |
||
publicRequest.requireApproval |
Delivery → intake | true |
Creates may sit awaiting staff price/approval (like slower portal intake) |
false |
Faster intake (still subject to pricing rules) | ||
publicRequest.autoDispatch |
Delivery → Auto-dispatch | true |
Nearby rider may be assigned on create — see Auto-assign |
false |
Staff (or your assign flow) must dispatch | ||
publicRequest.pricingMode / pricingAmountNgn |
Delivery → public pricing | fixed / starting_from |
Show amount when explaining fees; still don’t invent coverage |
coverage.* |
Delivery → Coverage | any | Soft hint only — create still validates addresses; out-of-area → coverage errors |
plan |
Billing | basic |
Fewer warehouses; service locations may be off |
pro |
Full features (see features) |
||
features.serviceLocations |
plan feature | false |
GET /service-locations returns [] — use geocoding / free-form carefully |
true but list empty |
Tell human to add service locations in the dashboard before chat bookings work well | ||
features.maxWarehouses |
plan | 1 |
Basic: only first warehouse is usable |
null |
Pro: no API-side warehouse cap | ||
rateLimits.requestsPerMinute |
Billing plan | number | Per-key cap for this org; HTTP 429 when exceeded |
null |
Custom / no per-key cap (Enterprise) |
Typed shape: API reference → GET /capabilities.
Annotated example
{
"ok": true,
"capabilities": {
"organization": { "id": "…", "name": "Campus Couriers", "slug": "campus" },
"plan": "pro",
"enabledServiceKinds": ["pickup_delivery"],
"defaultServiceKind": "pickup_delivery",
"allowWarehouseDropoff": true,
"errand": {
"enabledModes": ["at_place"],
"defaultMode": "at_place",
"allowUnknownShopLocation": false,
"curatedLinks": []
},
"paymentCollection": { "onlineCheckout": true, "cashOnDelivery": true },
"currency": "NGN",
"paymentTiming": "before_dispatch",
"coverage": {
"mode": "radius_from_hub",
"jobScopePolicy": "pickup_and_dropoff_in_coverage",
"radiusKm": 15,
"regionCount": 0
},
"pod": { "enabled": false },
"requireDeliveryOtp": true,
"publicRequest": {
"requireApproval": false,
"autoDispatch": true,
"pricingMode": "fixed",
"pricingAmountNgn": 1500
},
"features": { "serviceLocations": true, "maxWarehouses": null },
"rateLimits": { "requestsPerMinute": 180, "windowSeconds": 60 }
}
}{
"ok": true,
"capabilities": {
"organization": { "id": "…", "name": "Campus Couriers", "slug": "campus" },
"plan": "pro",
"enabledServiceKinds": ["pickup_delivery"],
"defaultServiceKind": "pickup_delivery",
"allowWarehouseDropoff": true,
"errand": {
"enabledModes": ["at_place"],
"defaultMode": "at_place",
"allowUnknownShopLocation": false,
"curatedLinks": []
},
"paymentCollection": { "onlineCheckout": true, "cashOnDelivery": true },
"currency": "NGN",
"paymentTiming": "before_dispatch",
"coverage": {
"mode": "radius_from_hub",
"jobScopePolicy": "pickup_and_dropoff_in_coverage",
"radiusKm": 15,
"regionCount": 0
},
"pod": { "enabled": false },
"requireDeliveryOtp": true,
"publicRequest": {
"requireApproval": false,
"autoDispatch": true,
"pricingMode": "fixed",
"pricingAmountNgn": 1500
},
"features": { "serviceLocations": true, "maxWarehouses": null },
"rateLimits": { "requestsPerMinute": 180, "windowSeconds": 60 }
}
}How an AI should read that example
- Only offer pickup & deliver (errand is off even though
errandobject exists —enabledServiceKindswins). - Warehouse pickup allowed → call
GET /warehouses. - Customer may pay online or cash; timing is before dispatch.
- No POD → never ask “collect item money from receiver.”
- Drop-off OTP required → customer needs the code (or your bot sends it via
delivery.otp_issued). - Auto-dispatch on → a rider may appear without staff Assign.
- Service locations on → peep hostel/gate coords from
GET /service-locations.
Warehouses
Dashboard: Settings → Warehouses (active rows only).
curl -X GET 'https://business.kiloapp.org/api/v1/warehouses' \ -H 'Authorization: Bearer sk_live_...'
curl -X GET 'https://business.kiloapp.org/api/v1/warehouses' \
-H 'Authorization: Bearer sk_live_...'const response = await fetch("https://business.kiloapp.org/api/v1/warehouses", {
method: "GET",
headers: {
"Authorization": "Bearer sk_live_..."
},
});
const data = await response.json();
console.log(data);const response = await fetch("https://business.kiloapp.org/api/v1/warehouses", {
method: "GET",
headers: {
"Authorization": "Bearer sk_live_..."
},
});
const data = await response.json();
console.log(data);import requests
url = "https://business.kiloapp.org/api/v1/warehouses"
headers = {
"Authorization": "Bearer sk_live_..."
}
response = requests.request("GET", url, headers=headers)
print(response.json())import requests
url = "https://business.kiloapp.org/api/v1/warehouses"
headers = {
"Authorization": "Bearer sk_live_..."
}
response = requests.request("GET", url, headers=headers)
print(response.json())<?php
$ch = curl_init("https://business.kiloapp.org/api/v1/warehouses");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer sk_live_...",
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;<?php
$ch = curl_init("https://business.kiloapp.org/api/v1/warehouses");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer sk_live_...",
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;| When | Action |
|---|---|
allowWarehouseDropoff is false |
Do not call this for pickup mode; do not send deliveryMode: "warehouse" |
true |
List warehouses; user picks one → create with deliveryMode: "warehouse", warehouseId, and pickupAddress / pickupCoords copied from that row’s address + longitude/latitude |
Same pins the public form uses for hub pickup.
Need the exact response shape right now? Jump to API reference → GET /warehouses.
Service locations (hostels, gates, landmarks)
Dashboard: Settings → Service locations (Pro). Same places the public form’s place picker uses.
Why bots need this: create requires real pickupCoords / deliveryCoords. Chat says *“Queen’s Hall room 12”* — not lat/lng. The business saves the place once with a map pin; the bot peeps coords and copies them into create. Do not invent {lng:0,lat:0} or re-geocode the composed address string (wrong building / out of coverage).
curl -X GET 'https://business.kiloapp.org/api/v1/service-locations' \ -H 'Authorization: Bearer sk_live_...'
curl -X GET 'https://business.kiloapp.org/api/v1/service-locations' \
-H 'Authorization: Bearer sk_live_...'const response = await fetch("https://business.kiloapp.org/api/v1/service-locations", {
method: "GET",
headers: {
"Authorization": "Bearer sk_live_..."
},
});
const data = await response.json();
console.log(data);const response = await fetch("https://business.kiloapp.org/api/v1/service-locations", {
method: "GET",
headers: {
"Authorization": "Bearer sk_live_..."
},
});
const data = await response.json();
console.log(data);import requests
url = "https://business.kiloapp.org/api/v1/service-locations"
headers = {
"Authorization": "Bearer sk_live_..."
}
response = requests.request("GET", url, headers=headers)
print(response.json())import requests
url = "https://business.kiloapp.org/api/v1/service-locations"
headers = {
"Authorization": "Bearer sk_live_..."
}
response = requests.request("GET", url, headers=headers)
print(response.json())<?php
$ch = curl_init("https://business.kiloapp.org/api/v1/service-locations");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer sk_live_...",
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;<?php
$ch = curl_init("https://business.kiloapp.org/api/v1/service-locations");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer sk_live_...",
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;unit.mode |
Allowed bot behavior |
|---|---|
none |
Use the place as-is |
free_text |
Ask for unit.label (e.g. Room); required if unit.required |
list |
Offer unit.ranges + unit.values; custom text only if unit.allowCustom |
Compose address text like the public form:
{name} — {address} — {unit}{name} — {address} — {unit}Keep the same place coords after the user picks a room (unit only changes the string).
Need the exact response shape right now? Jump to API reference → GET /service-locations.
Allowed vs not
| Situation | Allowed? |
|---|---|
Match user phrase to a service location → copy coords |
Yes (preferred for chat) |
| Free-form address + real geocoder when no place fits | Yes |
| Fake / hardcoded coordinates | No |
| Use service locations for an errand shop | No — follow errand.enabledModes for the shop |
| Use a service location for customer dropoff on an errand | Yes if they live in a known place |
Conversation pattern (bots)
GET /capabilities(cache for the session).- Build menus only from allowed values in the table above.
- If
pickup_delivery→ load service locations + warehouses; match vague place names; peepcoords. - If
errand→ errand modes only for the shop side; dropoff may still be a service location. - Choose
customerCollectionMethodonly if that payment flag istrue. - Then
POST /deliverieswith a complete payload.
If create returns service_disabled, payment errors, or coverage errors — re-fetch capabilities and tell the human which Delivery setting to change.
Related
- Auto-assign riders — when
publicRequest.autoDispatchis true - Assign riders — list roster riders and assign from your backend
- Create a delivery
- API reference
- Build with AI → WhatsApp bot