/pingNo authHealth check and the server clock. Use server_time to correct the phone's clock before recording offline actions.
Request
GET /api/inputmarket/v1/ping
Response
HTTP 200
{
"ok": true,
"server_time": "2026-11-06T16:17:00Z"
}How the Chomoka app, suppliers and offtakers connect to the Marketplace: savings groups order farm inputs on credit, suppliers deliver and are paid by the group's lender, and groups sell their harvest to offtakers.
https://market.stg.chomoka.org/developers/openapi.json
https://market.stg.chomoka.org/api/inputmarket/v1The API answers only on the Marketplace sub-domain, market. plus the environment's host. Ask Ensibuuko for the staging host and test credentials. All paths below are relative to the base URL.
config.currency; UGX has no decimals).Z; dates are YYYY-MM-DD.| Caller | Send on every request |
|---|---|
| Group app | Authorization: Basic base64(group_key:group_secret), the same credentials the rest of the app uses, and X-Device-Id. |
| Agent acting for a group | The group header plus X-Agent-Authority: <authority_token> from Agent sessions. Lasts 30 minutes by default; bound to the group and the phone. |
| Supplier or offtaker system | Authorization: Bearer <token> from phone sign-in. The phone must be registered by Ensibuuko for the company. Tokens last 30 days. |
| Wallet service | X-Inputmarket-Signature on the payment webhook. |
Codes sent by SMS expire after 10 minutes and allow 3 tries. The same phone can ask again after 30 seconds, at most 10 times an hour. Outside production every code may be a fixed test code; ask Ensibuuko for it.
| Header | Use |
|---|---|
Idempotency-Key | A UUID per write. Required on POST /orders, /delivery-confirmations, /evidence and /uploads; accepted on every write. Retrying with the same key and body returns the stored response with Idempotent-Replay: true. The same key with a different body returns 409 DUPLICATE_REQUEST. Server errors are not stored, so retry them with the same key. |
X-Device-Id | A stable id for the phone. Used for rate limits and to bind Agent sessions. |
X-Client-Created-At | When the action happened on the phone (ISO 8601). Send it with every action replayed from the offline queue. May also be sent as client_created_at in the body. |
Accept-Language | en, sw, fr, lg or rw. Texts come back in that language (Content-Language). |
If-None-Match | Group GET responses carry an ETag. Send it back to get 304 Not Modified. |
Every response carries X-Server-Time. Requests and responses are JSON (Accept: application/json), except photo uploads, which are multipart/form-data. Lists come back as {"data": [...], "meta": {"page", "per_page", "total"}} when paged; the catalogue lists are bare arrays unless you pass page.
Every error has the same shape. Show message to people; branch on code.
Example
HTTP 422
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Add at least one item.",
"details": {
"field": "lines"
}
}
}| Code | HTTP | Meaning and what to do |
|---|---|---|
UNAUTHENTICATED | 401 | Missing or wrong credentials, or an expired token. Sign in again. |
FORBIDDEN | 403 | Signed in but not allowed. details.reason may be AGENT_SESSION_EXPIRED, AGENT_NOT_ASSIGNED, AGENT_WRONG_DEVICE, AGENT_REQUIRED. |
NOT_ELIGIBLE | 403 | The Marketplace is not open to this group; details.reason says why. |
NOT_FOUND | 404 | No such item for this caller. |
NOT_SUPPORTED | 404 | The action exists in the design but is switched off (for example countering). |
VALIDATION_ERROR | 422 | Bad or missing input. details names the field or lists missing. |
INVALID_STATE | 409 | The item is not in a state that allows this (for example signing an expired offer). |
STALE_STATE | 409 | Something changed since the app last loaded it. Reload and retry. |
DUPLICATE_REQUEST | 409 | This Idempotency-Key was already used for a different request. |
CREDIT_LIMIT_EXCEEDED | 422 | The order needs more credit than the group has. |
PRODUCT_UNAVAILABLE | 422 | Out of stock, hidden, or not sold to this group. |
PRICE_CHANGED | 409 | A price changed since the quote. Quote again and show the difference. |
OFFER_EXPIRED | 409 | The offer can no longer be signed. |
ALREADY_SIGNED | 409 | This officer already signed. |
OTP_INVALID / OTP_EXPIRED / OTP_LOCKED | 422 | Wrong code, code too old (10 minutes), or too many wrong codes (3). Ask for a new code. |
RATE_LIMITED | 429 | Too many code requests. Wait before asking again. |
WALLET_INSUFFICIENT | 422 | The group wallet cannot cover the wallet part. |
PAYMENT_METHOD_NOT_ALLOWED | 422 | That payment method is switched off for this group. |
EVIDENCE_REQUIRED / EVIDENCE_EXPIRED | 409 / 422 | Required photos are missing, or a photo is too old for this step. |
HASH_MISMATCH / INTEGRITY_FAILED | 422 | An upload's checksum or content did not match. |
ALLOCATION_MISMATCH | 422 | Member shares do not add up to the order lines. |
MEMBER_INELIGIBLE | 422 | A member cannot receive inputs (inactive, or no plot when one is required). |
CONSENT_REQUIRED / CONSENT_CAP_EXCEEDED | 422 | A member's share has no valid consent, or is above the cap for that consent method. |
AGENT_ACTION_REQUIRES_LIVE | 422 | An Agent step was sent from the offline queue; do it again live. |
FUNDING_UNAVAILABLE / FUNDING_PENDING | 422 / 409 | No lender can fund this loan, or the lender has not approved yet. |
SERVER_ERROR | 500 | Our fault. Retry with the same Idempotency-Key. |
| Field | Values |
|---|---|
Order app_status | WAITING_FOR_SUPPLIER (also while a lender approves) → OFFER_READY → PREPARING → ON_THE_WAY → AWAITING_CONFIRMATION → CLOSED. display_status.tone is waiting, action, progress, warning, neutral or done. |
status.fulfilment | SUBMITTED, PROVIDER_REVIEW, OFFER_READY, OFFER_ACCEPTED, PREPARING, DISPATCHED, AWAITING_GROUP_CONFIRMATION, DELIVERED_CONFIRMED, CLOSED, DISPUTED, REJECTED, EXPIRED, DECLINED_BY_GROUP, CANCELLED |
status.payment | NOT_DUE, PENDING, PROCESSING, PAID, FAILED, HELD_FOR_REVIEW |
status.loan | NONE, RESERVED, ACTIVE, IN_ARREARS, REPAID |
| Commitment | DRAFT, PARTIALLY_SIGNED, BINDING, FULFILLED, CANCELLED, BREACHED |
| Pickup | SCHEDULED, ARRIVED, WEIGHED, CANCELLED, NO_SHOW |
| Weigh ticket | DRAFT, CONFIRMED, DISPUTED, REWEIGHED |
| Settlement | COMPUTED (offtaker to pay), BALANCE_PAID, GROUP_CONFIRMED |
| Price proposal / offer | OPEN, ACCEPTED, DECLINED, EXPIRED, WITHDRAWN |
Call these when the app opens and when the Marketplace tab is shown. /me/context works even when the group is not eligible, so the app can say why.
/pingNo authHealth check and the server clock. Use server_time to correct the phone's clock before recording offline actions.
Request
GET /api/inputmarket/v1/ping
Response
HTTP 200
{
"ok": true,
"server_time": "2026-11-06T16:17:00Z"
}/me/contextGroupEverything the home screen needs: the group and its location, eligibility, credit, wallet, allowed payment methods, consent status, counts of things waiting for the group, the officers who sign, and the limits the app applies before calling the server (config). With an Agent signed in, user and agent describe the Agent.
Request
GET /api/inputmarket/v1/me/context
HTTP 200
{
"group": {
"id": "g-100",
"name": "Women's Maize Group",
"country": "UG",
"location": {
"region": "Central",
"district": "Mukono",
"subcounty": "Goma",
"parish": "Seeta",
"village": "Bajjo"
},
"geo": {
"lat": 0.3601,
"lng": 32.7514
}
},
"user": {
"id": null,
"role": "GROUP",
"name": "Women's Maize Group"
},
"agent": null,
"eligible": true,
"ineligible_reason": null,
"capabilities": [
"order.create"
],
"credit": {
"limit": 5000000,
"used": 0,
"reserved": 0,
"available": 5000000
},
"wallet": {
"balance": 450000,
"held": 0,
"available": 450000,
"has_wallet": true
},
"payment_methods": [
"LOAN"
],
"consent": {
"accepted": false,
"version": "2",
"current_version": "2"
},
"action_counts": {
"offers_to_review": 0,
"deliveries_to_confirm": 0
},
"contacts": [
{
"id": "101",
"kind": "ACCOUNT_OWNER",
"name": "Mary Akello",
"role": "CHAIRPERSON",
"phone": "+256 7•• ••• 101"
}
],
"farm": {
"completeness_pct": 0,
"plots_count": 0,
"expected_kg": 0
},
"unsent_photo_alert": null,
"unsent_photo_alert_days": 7,
"product_name": "Marketplace",
"config": {
"allocation": {
"require_mapped_plot": false
},
"consent": {
"witnessed_max_amount": 300000,
"require_agent": false
},
"farm": {
"buyer_visibility_min_percent": 80,
"area_match_tolerance_percent": 20,
"gps_max_accuracy_m": null,
"checkin_required_photos": null
},
"agent": {
"session_minutes": 30
}
}
}/configGroupAll runtime settings for this group: currency, feature switches, cart limits, offer and delivery rules, dispute reasons, support contacts, texts, evidence policies, consent methods, OTP and offline settings. Large; cache it and send If-None-Match with the last ETag to get 304 Not Modified.
Request
GET /api/inputmarket/v1/config
If-None-Match: "c5117bd7d9ffdd565b3ebfe1f1f1834d405455e0"Response
HTTP 200 (or 304 with an empty body when nothing changed)
{
"currency": { "code": "UGX", "symbol": "UGX", "decimals": 0, "thousands_sep": "," },
"features": { "inputmarket_enabled": true, "buyback_enabled": false, "cash_component_enabled": false, "disputes": true, "direct_repayment": false },
"cart": { "multi_provider": true, "max_lines": 20 },
"credit": { … }, "funding": { … }, "offer": { … }, "cancel": { … }, "delivery": { … },
"dispute_reasons": [ … ], "agent": { … }, "support": { … }, "texts": { … },
"payment": { … }, "repayment": { … }, "evidence": { … }, "farm": { … }, "offtake": { … },
"allocation": { … }, "consent": { … }, "otp": { … }, "offline": { … }, "uploads": { … },
"version": "…"
}/creditGroupThe group's Marketplace credit on its own: limit, used by running loans, reserved by open orders, available.
Request
GET /api/inputmarket/v1/credit
Response
HTTP 200
{
"limit": 5000000,
"used": 0,
"reserved": 0,
"available": 5000000
}/consentGroupRecords that the group accepted the Marketplace terms. Send the current_version from /me/context.
| Body field | Type | Meaning |
|---|---|---|
version | string | The consent text version shown to the group. |
/devicesGroupRegisters the phone (push token, app version). Optional.
/notificationsGroupMessages for the group, newest first, paged.
| Query | Type | Meaning |
|---|---|---|
page | int | Page, from 1. |
per_page | int | Items per page (default 20). |
Request
GET /api/inputmarket/v1/notifications
Response
HTTP 200
{
"data": [],
"meta": {
"page": 1,
"per_page": 20,
"total": 0
}
}/notifications/{id}/readGroupMarks one notification read.
Every phone number saved in the Marketplace is confirmed first. A code goes to the phone, and the person who has it reads it back. Then the name on the number's mobile money account is compared with the name given. Only after the code is confirmed does the server look the name up, so these endpoints never reveal who owns a number. Where no wallet API covers the country (outside Uganda today), the code alone confirms the phone and name_checked is false.
A confirmed check returns a token. Send it with the phone where the phone is saved: carrier.phone_token on dispatches, rep.phone_token on pickups, receiver.phone_token on delivery confirmations (not needed when the receiver is the officer who typed the delivery code), phone_token on team members, contact.phone_token on company contacts. A token works once, for that phone and purpose, for 30 minutes. A missing or wrong token returns 422 VALIDATION_ERROR with details.field ending in phone_token.
The same three endpoints are under /provider/, /offtaker/ and /admin/ with those tokens. Member phones in the group app use their own flow (/members/{id}/phones), which runs the same name check.
/phone-checksGroupSends a code to the phone. purpose is one of DRIVER, PICKUP_REP, DELIVERY_RECEIVER, TEAM_MEMBER, COMPANY_CONTACT, JOIN.
| Body field | Type | Meaning |
|---|---|---|
purpose | string | What the phone is for. |
phone | string | International, or local for country. |
country optional | string | ISO code for a local number. |
name | string | The full name of the person who uses the phone, as on their mobile money account. |
Request
POST /api/inputmarket/v1/phone-checks
{
"purpose": "DRIVER",
"phone": "0772 123 456",
"country": "UG",
"name": "David Mukasa"
}Response
HTTP 202
{
"id": "9f3c1a52-…",
"status": "CODE_SENT",
"phone": "+256772123456",
"masked_phone": "+256 7•• ••• 456",
"expires_at": "2026-11-06T10:40:00Z"
}/phone-checks/{id}/confirmGroupChecks the code, then the mobile money name. VERIFIED returns the token. NAME_MISMATCH returns the registered name and no token: ask for a phone in the right name, or save the phone under the registered name. If the wallet service does not answer you get 503 with details.retry_without_code: true; call again without a code. A number with no mobile money account returns 422.
| Body field | Type | Meaning |
|---|---|---|
code | string | The code sent to the phone. Leave it out when retrying after a 503. |
Request
POST /api/inputmarket/v1/phone-checks/9f3c1a52-…/confirm
{ "code": "482913" }Response
HTTP 200
{
"id": "9f3c1a52-…",
"status": "VERIFIED",
"token": "Qm4x…48 characters…",
"phone": "+256772123456",
"name": "David Mukasa",
"registered_name": "MUKASA DAVID",
"name_checked": true,
"message": "Phone confirmed. The mobile money account is in the name MUKASA DAVID."
}
or, when the names differ:
{
"id": "9f3c1a52-…",
"status": "NAME_MISMATCH",
"registered_name": "OKELLO JOHN",
"name_checked": true,
"message": "This number is registered to OKELLO JOHN, not David Mukasa. …"
}/phone-checks/{id}/use-registered-nameGroupAfter NAME_MISMATCH: confirms the phone under the name on its mobile money account. The returned name is what will be saved, whatever name the form sends.
/provider/phone-checksSupplierSame as POST /phone-checks, for supplier integrations (drivers, team members, company contact).
/provider/phone-checks/{id}/confirmSupplierSame as POST /phone-checks/{id}/confirm.
/offtaker/phone-checksOfftakerSame as POST /phone-checks, for offtaker integrations (pickup reps).
/offtaker/phone-checks/{id}/confirmOfftakerSame as POST /phone-checks/{id}/confirm.
Prices here are what the group pays (unit_price, Ensibuuko's fee included). Supplier prices are never sent to the group app. Lists are a bare JSON array unless you pass page, which switches to {data, meta}.
/categoriesGroupProduct categories.
Request
GET /api/inputmarket/v1/categories
Response
HTTP 200
[
{
"id": "a32964e9-cc8a-4c24-83fe-ea7c8267876f",
"code": "seeds",
"name": "Seeds",
"icon_url": null,
"parent_id": null
}
]/productsGroupProducts the group can order, from suppliers serving its country and district.
| Query | Type | Meaning |
|---|---|---|
category_id | uuid | Only this category. |
q | string | Search by name. |
sort | string | Sort order. |
page, per_page | int | Ask for a paged {data, meta} response. |
Product photos. images lists every photo, cover first: {url, thumb_url, width, height}. url is a 1024 × 1024 JPEG and thumb_url a 320 × 320 JPEG. URLs never change, so cache them. image_urls (large URLs only) stays for older app versions.
Request
GET /api/inputmarket/v1/products
Response
HTTP 200
[
{
"id": "a32964e9-d193-4057-a886-2f8ac0970869",
"provider": {
"id": "a32964e9-ce56-46db-bac9-00e337495b2c",
"name": "Agro Supply",
"logo_url": null
},
"category_id": "a32964e9-cc8a-4c24-83fe-ea7c8267876f",
"name": "Bean seed",
"description": null,
"unit": {
"code": "kg",
"name": "Kilogram"
},
"unit_price": 6000,
"availability": "IN_STOCK",
"min_qty": 1,
"max_qty": 500,
"qty_step": 1,
"image_urls": [],
"images": [],
"delivery_terms": null
}
]/products/{id}GroupOne product.
Request
GET /api/inputmarket/v1/products/a32964e9-cf34-4db6-a3d0-79e3a2eb5b87
Response
HTTP 200
{
"id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"provider": {
"id": "a32964e9-ce56-46db-bac9-00e337495b2c",
"name": "Agro Supply",
"logo_url": null
},
"category_id": "a32964e9-cc8a-4c24-83fe-ea7c8267876f",
"name": "Maize seed (Longe 5)",
"description": "Open-pollinated maize.",
"unit": {
"code": "kg",
"name": "Kilogram"
},
"unit_price": 105000,
"availability": "IN_STOCK",
"min_qty": 1,
"max_qty": 500,
"qty_step": 1,
"image_urls": [],
"images": [],
"delivery_terms": "3 to 5 days."
}/providersGroupSuppliers the group can buy from.
Request
GET /api/inputmarket/v1/providers
Response
HTTP 200
[
{
"id": "a32964e9-ce56-46db-bac9-00e337495b2c",
"name": "Agro Supply",
"logo_url": null
}
]/providers/{id}GroupOne supplier.
Before an order is sent the app quotes it, splits it between members, and collects each member's consent to their share. The draft's Idempotency-Key is the subject_id of every consent and the key of the final POST /orders.
/cart/quoteGroupPrices a cart without saving it. Returns one order per supplier, the wallet/loan split, the lender, and whether the group's credit covers it (exceeds_limit, shortfall).
| Body field | Type | Meaning |
|---|---|---|
lines | array | [{product_id, qty}] |
payment_plan optional | object | {method: LOAN | WALLET | SPLIT, wallet_amount?} |
allocations optional | array | Member split, to validate it. |
Request
POST /api/inputmarket/v1/cart/quote
Idempotency-Key: fd759bab-b9e0-47ce-9a5e-f8cf0f96cf8c
{
"lines": [
{
"product_id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"qty": 10
}
],
"payment_plan": {
"method": "LOAN"
}
}HTTP 200
{
"orders": [
{
"provider_id": "a32964e9-ce56-46db-bac9-00e337495b2c",
"provider": {
"id": "a32964e9-ce56-46db-bac9-00e337495b2c",
"name": "Agro Supply",
"logo_url": null
},
"lines": [
{
"product_id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"name": "Maize seed (Longe 5)",
"unit": "kg",
"qty": 10,
"unit_price": 105000,
"line_total": 1050000,
"availability": "IN_STOCK"
}
],
"total": 1420000,
"wallet_amount": 0,
"loan_amount": 1420000,
"funder": {
"id": "a32964e9-ea4c-417b-9286-4b1d5a4ba144",
"name": "Ensibuuko Finance",
"available": true
}
}
],
"grand_total": 1420000,
"total": 1420000,
"payment": {
"method": "LOAN",
"wallet_amount": 0,
"loan_amount": 1420000
},
"wallet_amount": 0,
"loan_amount": 1420000,
"wallet_balance_after": 450000,
"credit_after": 3580000,
"credit": {
"limit": 5000000,
"used": 0,
"reserved": 0,
"available": 5000000,
"after": 3580000,
"exceeds": false,
"shortfall": 0,
"suggested_max_total": 5000000
},
"available_after": 3580000,
"exceeds_limit": false,
"shortfall": 0,
"allocations": [],
"warnings": []
}/membersGroupMembers of the group. id is the app's own member id (memberID). Phones are masked.
Request
GET /api/inputmarket/v1/members
Response
HTTP 200
{
"data": [
{
"id": "101",
"name": "Mary Akello",
"member_number": 101,
"phone": "+256 7•• ••• 101",
"has_phone": true,
"active": true,
"officer_role": "CHAIRPERSON",
"eligible": true,
"ineligible_reason": null
}
]
}/orders/allocations/suggestGroupProposes how to split each line between members. subject_hash is what that member's consent must cover.
| Body field | Type | Meaning |
|---|---|---|
lines | array | [{product_id, qty}] |
member_ids | array | Members receiving inputs. |
Request
POST /api/inputmarket/v1/orders/allocations/suggest
Idempotency-Key: 1d4bf020-8247-4606-a348-11afc9f55c89
{
"lines": [
{
"product_id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"qty": 10
}
],
"member_ids": [
"101"
]
}Response
HTTP 200
{
"mode": "EVEN",
"allocations": [
{
"member_id": "101",
"lines": [
{
"product_id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"qty": 4
}
],
"amount": 605000,
"subject_hash": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87:4|a32964e9-d0f9-4681-9fae-c08e97079f31:1"
}
]
}/members/{id}/consent-optionsGroupHow this member can consent to an amount: own phone, a shared phone, or witnessed by an Agent or officer, each with its cap and whether it is available.
| Query | Type | Meaning |
|---|---|---|
amount | int | The member's share in currency units. |
Request
GET /api/inputmarket/v1/members/105/consent-options?amount=50000
Response
HTTP 200
{
"member_id": "105",
"amount": 50000,
"options": [
{
"method": "OWN_PHONE",
"assurance": "HIGH",
"available": false,
"cap": null,
"remaining_cap": null,
"masked_phone": null,
"reason": "NO_PHONE"
}
]
}/consents/challengeGroupSends a code to the member's phone for their share. Use purpose: PLEDGE with subject_type: LISTING and qty_kg for harvest pledges.
| Body field | Type | Meaning |
|---|---|---|
member_id | string | Member id. |
purpose optional | string | ALLOCATION (default) or PLEDGE. |
subject_type optional | string | ORDER_DRAFT or LISTING. |
subject_id | string | The draft's Idempotency-Key, or the listing id. |
allocation optional | array | [{product_id, qty}] for ALLOCATION. |
qty_kg optional | number | For PLEDGE. |
Request
POST /api/inputmarket/v1/consents/challenge
Idempotency-Key: edef1d5b-5886-4338-9860-f0f30d7701db
{
"member_id": "101",
"purpose": "ALLOCATION",
"subject_type": "ORDER_DRAFT",
"subject_id": "4f721602-eb57-4df4-b091-b109d738b071",
"allocation": [
{
"product_id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"qty": 4
}
]
}Response
HTTP 201
{
"challenge_id": "a32964e9-f0b8-44df-838b-e511d80314c8",
"masked_phone": "+256 7•• ••• 101",
"summary_text": "4 kg Maize seed (Longe 5), 1 bag DAP fertiliser 50 kg worth 605,000",
"subject_hash": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87:4|a32964e9-d0f9-4681-9fae-c08e97079f31:1",
"method": "OWN_PHONE",
"expires_at": "2026-11-06T16:27:00Z",
"id": "a32964e9-f0b8-44df-838b-e511d80314c8",
"amount": 605000
}/consents/verifyGroupChecks the member's code and records the consent.
| Body field | Type | Meaning |
|---|---|---|
challenge_id | uuid | |
otp | string | The code the member received. |
Request
POST /api/inputmarket/v1/consents/verify
Idempotency-Key: 4b4a973f-e234-432b-b504-d72adbcc7b22
{
"challenge_id": "a32964e9-f0b8-44df-838b-e511d80314c8",
"otp": "235832"
}Response
HTTP 200
{
"consent_id": "a32964e9-f315-4748-aa61-90312df8186f",
"member_id": "101",
"subject_hash": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87:4|a32964e9-d0f9-4681-9fae-c08e97079f31:1",
"method": "OWN_PHONE",
"assurance": "HIGH",
"status": "VERIFIED"
}/consents/witnessedGroup + AgentConsent for a member with no phone, witnessed by the signed-in Agent with a signature and a photo. Needs the Agent capability witness.sign. Capped by config.consent.witnessed_max_amount.
| Body field | Type | Meaning |
|---|---|---|
member_id | string | |
subject_id | string | Draft key. |
allocation | array | |
signature_evidence_id | uuid | Uploaded signature. |
photo_evidence_id | uuid | Uploaded member photo. |
witness_user_ids optional | array |
/consents/{id}/withdrawGroupWithdraws a consent before the order is sent.
/members/{id}/phonesGroupAdds a phone for a member (own or shared) and sends it a code.
| Body field | Type | Meaning |
|---|---|---|
msisdn | string | Phone number. |
kind | string | OWN or SHARED. |
holder_name optional | string | Who holds a shared phone. |
relationship optional | string |
/members/{id}/phones/verifyGroupConfirms the new phone with its code, then checks the name on its mobile money account. An OWN phone must be in the member's name; otherwise 422 with details.registered_name, and the app should offer to add it as a shared phone. A SHARED phone takes the holder's real name. Returns holder_name, registered_name and name_checked.
| Body field | Type | Meaning |
|---|---|---|
challenge_id | uuid | |
otp | string |
One checkout can create several orders, one per supplier. Show display_status.label to people; branch on app_status in code. next_action says what the group must do, if anything.
/ordersGroupSends the order. Idempotency-Key is required and must equal the draft key used for the consents. Every member share needs a consent.
| Body field | Type | Meaning |
|---|---|---|
lines | array | [{product_id, qty}] |
allocations | array | [{member_id, lines:[{product_id, qty}]}] |
delivery | object | {use_group_location, location_id?, landmark?, preferred_window?} |
payment_plan | object | {method: LOAN | WALLET | SPLIT, wallet_amount?} |
Request
POST /api/inputmarket/v1/orders
Idempotency-Key: 4f721602-eb57-4df4-b091-b109d738b071
{
"lines": [
{
"product_id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"qty": 10
}
],
"allocations": [
{
"member_id": "101",
"lines": [
{
"product_id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"qty": 4
}
]
}
],
"delivery": {
"use_group_location": true,
"landmark": "Near the church",
"preferred_window": "Next week"
},
"payment_plan": {
"method": "LOAN"
}
}HTTP 201
{
"checkout_id": "bea0bdf3-1653-43b5-866f-55f7f3b494ae",
"orders": [
{
"id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"reference": "IM-2026-000001",
"provider": {
"id": "a32964e9-ce56-46db-bac9-00e337495b2c",
"name": "Agro Supply"
},
"provider_name": "Agro Supply",
"total": 1420000,
"status": {
"fulfilment": "SUBMITTED",
"payment": "NOT_DUE",
"loan": "NONE"
},
"app_status": "WAITING_FOR_SUPPLIER",
"display_status": {
"label": "Waiting for the supplier",
"tone": "waiting"
},
"next_action": null,
"created_at": "2026-11-06T16:18:33Z",
"checkout_id": "bea0bdf3-1653-43b5-866f-55f7f3b494ae",
"payment_method": "LOAN",
"wallet_amount": 0,
"loan_amount": 1420000,
"lines": [
{
"id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"product_id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"name": "Maize seed (Longe 5)",
"unit": "kg",
"unit_price": 105000,
"qty_ordered": 10,
"qty_confirmed": null,
"qty_dispatched": 0,
"qty_delivered": 0,
"qty_received": 0,
"qty_rejected": 0,
"line_total": 1050000
}
],
"delivery": {
"use_group_location": true,
"location_id": null,
"hierarchy": {
"region": "Central",
"district": "Mukono",
"subcounty": "Goma",
"parish": "Seeta",
"village": "Bajjo"
},
"geo": {
"lat": 0.3601,
"lng": 32.7514
},
"landmark": "Near the church",
"address": null,
"preferred_window": "Next week",
"receiver": {
"name": "Mary Akello",
"phone": "+256772000101"
}
},
"offer_expires_at": null,
"timeline": [
{
"status": "SUBMITTED",
"event": "SUBMITTED",
"at": "2026-11-06T16:18:33Z",
"on_behalf_of_agent": null
}
],
"dispatches": [],
"on_behalf_of_agent": null,
"loan": null,
"close_reason": null,
"note": null
}
],
"reference": "IM-2026-000001",
"total": 1420000
}/ordersGroupThe group's orders, paged.
| Query | Type | Meaning |
|---|---|---|
bucket | string | active, needs_action or done. |
page, per_page | int |
Request
GET /api/inputmarket/v1/orders?bucket=active
HTTP 200
{
"data": [
{
"id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"reference": "IM-2026-000001",
"provider": {
"id": "a32964e9-ce56-46db-bac9-00e337495b2c",
"name": "Agro Supply"
},
"provider_name": "Agro Supply",
"total": 1420000,
"status": {
"fulfilment": "SUBMITTED",
"payment": "NOT_DUE",
"loan": "NONE"
},
"app_status": "WAITING_FOR_SUPPLIER",
"display_status": {
"label": "Waiting for the supplier",
"tone": "waiting"
},
"next_action": null,
"created_at": "2026-11-06T16:18:33Z"
}
],
"meta": {
"page": 1,
"per_page": 20,
"total": 1
}
}/orders/{id}GroupOne order with lines, delivery, timeline, dispatches and, once delivered, its loan.
Request
GET /api/inputmarket/v1/orders/a32964e9-fe70-4e98-9dde-56d03dd64881
HTTP 200
{
"id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"reference": "IM-2026-000001",
"provider": {
"id": "a32964e9-ce56-46db-bac9-00e337495b2c",
"name": "Agro Supply"
},
"provider_name": "Agro Supply",
"total": 1420000,
"status": {
"fulfilment": "CLOSED",
"payment": "PAID",
"loan": "ACTIVE"
},
"app_status": "CLOSED",
"display_status": {
"label": "Completed",
"tone": "done"
},
"next_action": null,
"created_at": "2026-11-06T16:18:33Z",
"checkout_id": "bea0bdf3-1653-43b5-866f-55f7f3b494ae",
"payment_method": "LOAN",
"wallet_amount": 0,
"loan_amount": 1420000,
"lines": [
{
"id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"product_id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"name": "Maize seed (Longe 5)",
"unit": "kg",
"unit_price": 105000,
"qty_ordered": 10,
"qty_confirmed": 10,
"qty_dispatched": 10,
"qty_delivered": 10,
"qty_received": 10,
"qty_rejected": 0,
"line_total": 1050000
}
],
"delivery": {
"use_group_location": true,
"location_id": null,
"hierarchy": {
"region": "Central",
"district": "Mukono",
"subcounty": "Goma",
"parish": "Seeta",
"village": "Bajjo"
},
"geo": {
"lat": 0.3601,
"lng": 32.7514
},
"landmark": "Near the church",
"address": null,
"preferred_window": "Next week",
"receiver": {
"name": "Mary Akello",
"phone": "+256772000101"
}
},
"offer_expires_at": "2026-11-09T16:18:33Z",
"timeline": [
{
"status": "SUBMITTED",
"event": "SUBMITTED",
"at": "2026-11-06T16:18:33Z",
"on_behalf_of_agent": null
}
],
"dispatches": [
{
"id": "a32964ea-3019-489d-a64c-96ff8c9182e2",
"lines": [
{
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"qty": 10
}
],
"carrier": {
"name": "Driver",
"phone": "+256700000000",
"vehicle": "UAX 123B"
},
"dispatched_at": "2026-11-06T16:19:35Z",
"eta_from": null,
"eta_to": null,
"notes": null
}
],
"on_behalf_of_agent": null,
"loan": {
"id": "a32964ea-4fe5-4e07-b333-4756b3abda9f",
"order_id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"order_ref": "IM-2026-000001",
"provider_name": "Agro Supply",
"principal": 1420000,
"total_payable": 1420000,
"repaid": 0,
"outstanding": 1420000,
"status": "ACTIVE",
"activated_at": "2026-11-06T16:19:35Z",
"due_date": "2027-04-06",
"repayment_mode": "MONTHLY_INSTALMENTS",
"interest_rate_pct": 0,
"schedule": [
{
"due_date": "2026-12-06",
"amount": 284000,
"paid": 0,
"status": "DUE"
}
],
"next_due": {
"due_date": "2026-12-06",
"amount": 284000
}
},
"close_reason": "COMPLETED",
"note": null
}/orders/{id}/allocationsGroupWho receives what in this order.
Request
GET /api/inputmarket/v1/orders/a32964e9-fe70-4e98-9dde-56d03dd64881/allocations
Response
HTTP 200
{
"data": [
{
"id": "a32964e9-ff9b-4920-abb9-44c20cb0eef0",
"member_id": "101",
"name": "Mary Akello",
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"qty_requested": 4,
"qty_confirmed": 4,
"qty_received": null,
"amount": 420000,
"attributed_amount": null,
"status": "REQUESTED",
"consent_id": "a32964e9-f315-4748-aa61-90312df8186f",
"plot_id": null
}
]
}/orders/{id}/cancelGroupCancels before the supplier starts preparing.
| Body field | Type | Meaning |
|---|---|---|
reason optional | string |
After the supplier confirms, the group reviews the final items, the loan and its repayment plan. The number of officers in required_signatures each sign with a code sent to their own phone. When the order needs a lender's approval, the offer appears only after the lender approves.
/orders/{id}/offerGroupThe offer to review: confirmed lines, wallet and loan amounts, lender, repayment schedule preview (including any loan the group already repays), credit before and after, signatories and members receiving.
Request
GET /api/inputmarket/v1/orders/a32964e9-fe70-4e98-9dde-56d03dd64881/offer
HTTP 200
{
"offer_id": "a32964ea-1140-4c26-afe9-fee9d73b823f",
"order_id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"reference": "IM-2026-000001",
"provider_name": "Agro Supply",
"version": 1,
"status": "PENDING",
"expires_at": "2026-11-09T16:18:33Z",
"expires_in_hours": 72,
"order_summary": [
{
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"name": "Maize seed (Longe 5)",
"unit": "kg",
"unit_price": 105000,
"qty_ordered": 10,
"qty_confirmed": 10,
"line_total": 1050000
}
],
"lines": [
{
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"name": "Maize seed (Longe 5)",
"unit": "kg",
"unit_price": 105000,
"qty_ordered": 10,
"qty_confirmed": 10,
"line_total": 1050000
}
],
"items_total": 1420000,
"loan_amount_up_to": 1420000,
"loan_amount": 1420000,
"wallet_amount": 0,
"payment_method": "LOAN",
"lender_name": "Ensibuuko Finance",
"funding_status": "APPROVED",
"tenor_days": 150,
"repayment_note": "Repay a fixed amount every month. If you already have a running loan, your next monthly repayment includes this Marketplace loan once it is disbursed. Selling your harvest does not change what you owe.",
"terms": {
"financing_profile_id": "a32964e9-ce19-4d56-ac2a-a90533261ff2",
"tenor_days": 150,
"grace_days": 0,
"repayment_mode": "MONTHLY_INSTALMENTS",
"instalments": 5,
"instalment_amount": 284000,
"first_due_date": "2026-12-06",
"schedule_preview": [
{
"due_date": "2026-12-06",
"principal": 284000,
"interest": 0,
"amount": 284000
}
],
"combined_repayment_preview": {
"existing_loan_instalment": 0,
"marketplace_instalment": 284000,
"total": 284000,
"due_date": "2026-12-06",
"breakdown": [
{
"kind": "MARKETPLACE",
"loan_id": null,
"label": "Marketplace IM-2026-000001",
"amount": 284000,
"due_date": "2026-12-06"
}
]
},
"interest_rate_pct": 0,
"plain_language": "Repay a fixed amount every month. If you already have a running loan, your next monthly repayment includes this Marketplace loan once it is disbursed. Selling your harvest does not change what you owe.",
"text_blocks": [],
"agreement_url": null
},
"credit": {
"before": 5000000,
"after": 3580000
},
"credit_before": 5000000,
"credit_after": 3580000,
"required_signatures": 2,
"signatures": {
"required": 2,
"collected": 0,
"signed": [],
"eligible_signers": [
"101"
]
},
"signatories": [
{
"id": "101",
"name": "Mary Akello",
"role": "CHAIRPERSON",
"phone": "+256 7•• ••• 101",
"signed": false
}
],
"members_receiving": [
{
"member_id": "101",
"name": "Mary Akello",
"amount": 605000,
"lines": [
{
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"qty_requested": 4,
"qty_confirmed": 4
}
],
"status": "REQUESTED",
"consent_status": "CONSENTED"
}
],
"changes": [],
"final_amount_note": "Final loan = delivered quantity × price."
}/orders/{id}/offer/otpGroupSends a code to one officer.
| Body field | Type | Meaning |
|---|---|---|
signatory_id | string | Officer's member id. |
Request
POST /api/inputmarket/v1/orders/a32964e9-fe70-4e98-9dde-56d03dd64881/offer/otp
Idempotency-Key: 180f5dba-6262-4e71-91b4-34df93699ba0
{
"signatory_id": "101"
}Response
HTTP 201
{
"challenge_id": "a32964ea-1672-4de7-9026-08bd3038a316",
"id": "a32964ea-1672-4de7-9026-08bd3038a316",
"masked_phone": "+256 7•• ••• 101",
"expires_at": "2026-11-06T16:28:33Z"
}/orders/{id}/offer/signGroupOne officer signs. When enough have signed, status becomes ACCEPTED and the supplier prepares the order.
| Body field | Type | Meaning |
|---|---|---|
signatory_id | string | |
challenge_id | uuid | |
otp | string | |
accepted_terms | bool | Must be true. |
Request
POST /api/inputmarket/v1/orders/a32964e9-fe70-4e98-9dde-56d03dd64881/offer/sign
Idempotency-Key: 8b90fe13-8240-4d42-bd19-8698deac9d18
{
"signatory_id": "102",
"challenge_id": "a32964ea-1c66-4931-b2a2-5326cb90281c",
"otp": "359449",
"accepted_terms": true
}HTTP 200
{
"offer_id": "a32964ea-1140-4c26-afe9-fee9d73b823f",
"order_id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"reference": "IM-2026-000001",
"provider_name": "Agro Supply",
"version": 1,
"status": "ACCEPTED",
"expires_at": "2026-11-09T16:18:33Z",
"expires_in_hours": 72,
"order_summary": [
{
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"name": "Maize seed (Longe 5)",
"unit": "kg",
"unit_price": 105000,
"qty_ordered": 10,
"qty_confirmed": 10,
"line_total": 1050000
}
],
"lines": [
{
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"name": "Maize seed (Longe 5)",
"unit": "kg",
"unit_price": 105000,
"qty_ordered": 10,
"qty_confirmed": 10,
"line_total": 1050000
}
],
"items_total": 1420000,
"loan_amount_up_to": 1420000,
"loan_amount": 1420000,
"wallet_amount": 0,
"payment_method": "LOAN",
"lender_name": "Ensibuuko Finance",
"funding_status": "APPROVED",
"tenor_days": 150,
"repayment_note": "Repay a fixed amount every month. If you already have a running loan, your next monthly repayment includes this Marketplace loan once it is disbursed. Selling your harvest does not change what you owe.",
"terms": {
"financing_profile_id": "a32964e9-ce19-4d56-ac2a-a90533261ff2",
"tenor_days": 150,
"grace_days": 0,
"repayment_mode": "MONTHLY_INSTALMENTS",
"instalments": 5,
"instalment_amount": 284000,
"first_due_date": "2026-12-06",
"schedule_preview": [
{
"due_date": "2026-12-06",
"principal": 284000,
"interest": 0,
"amount": 284000
}
],
"combined_repayment_preview": {
"existing_loan_instalment": 0,
"marketplace_instalment": 284000,
"total": 284000,
"due_date": "2026-12-06",
"breakdown": [
{
"kind": "MARKETPLACE",
"loan_id": null,
"label": "Marketplace IM-2026-000001",
"amount": 284000,
"due_date": "2026-12-06"
}
]
},
"interest_rate_pct": 0,
"plain_language": "Repay a fixed amount every month. If you already have a running loan, your next monthly repayment includes this Marketplace loan once it is disbursed. Selling your harvest does not change what you owe.",
"text_blocks": [],
"agreement_url": null
},
"credit": {
"before": 5000000,
"after": 3580000
},
"credit_before": 5000000,
"credit_after": 3580000,
"required_signatures": 2,
"signatures": {
"required": 2,
"collected": 2,
"signed": [
{
"id": "101",
"name": "Mary Akello",
"role": "CHAIRPERSON",
"at": "2026-11-06T16:19:04Z"
}
],
"eligible_signers": [
"101"
]
},
"signatories": [
{
"id": "101",
"name": "Mary Akello",
"role": "CHAIRPERSON",
"phone": "+256 7•• ••• 101",
"signed": true
}
],
"members_receiving": [
{
"member_id": "101",
"name": "Mary Akello",
"amount": 605000,
"lines": [
{
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"qty_requested": 4,
"qty_confirmed": 4
}
],
"status": "ACCEPTED",
"consent_status": "CONSENTED"
}
],
"changes": [],
"final_amount_note": "Final loan = delivered quantity × price.",
"order_status": "OFFER_ACCEPTED"
}/orders/{id}/offer/declineGroupThe group declines the offer. The order closes and the reserved credit is released.
| Body field | Type | Meaning |
|---|---|---|
reason optional | string |
The group confirms what arrived, line by line, with photos, a signature, GPS and an officer's code. The supplier is paid and the loan starts only for what the group received.
/orders/{id}/dispatchesGroupWhat the supplier sent, with the carrier.
Request
GET /api/inputmarket/v1/orders/a32964e9-fe70-4e98-9dde-56d03dd64881/dispatches
Response
HTTP 200
{
"data": [
{
"id": "a32964ea-3019-489d-a64c-96ff8c9182e2",
"lines": [
{
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"qty": 10
}
],
"carrier": {
"name": "Driver",
"phone": "+256700000000",
"vehicle": "UAX 123B"
},
"dispatched_at": "2026-11-06T16:19:35Z",
"eta_from": null,
"eta_to": null,
"notes": null
}
]
}/orders/{id}/delivery-confirmation/templateGroupThe confirmation form: lines with quantities delivered, the photo policy and what is still missing, reasons for refusing items, and officers who can confirm.
Request
GET /api/inputmarket/v1/orders/a32964e9-fe70-4e98-9dde-56d03dd64881/delivery-confirmation/template
HTTP 200
{
"order_id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"reference": "IM-2026-000001",
"status": "AWAITING_GROUP_CONFIRMATION",
"lines": [
{
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"name": "Maize seed (Longe 5)",
"unit": "kg",
"unit_price": 105000,
"qty_confirmed": 10,
"qty_dispatched": 10,
"qty_delivered_by_provider": 10
}
],
"require_otp": true,
"confirm_roles": [
"OFFICER"
],
"evidence": {
"policy": {
"actor": "GROUP",
"per_order_line": {
"min": 1
},
"when_rejected_qty_gt_0": {
"slot": "damage_closeup",
"min": 1
},
"optional": [
"group_with_goods"
],
"signature": true,
"gps": true,
"max_distance_m": 300
},
"legacy": {
"photo": {
"required": false,
"max": 3
},
"signature": {
"required": true
},
"gps": {
"required": false
}
},
"missing": [
{
"slot": "line_photo",
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"min": 1,
"have": 0
}
]
},
"exception_reasons": [
{
"code": "SHORT",
"label": "Short delivery"
}
],
"signatories": [
{
"id": "101",
"name": "Mary Akello",
"role": "CHAIRPERSON",
"phone": "+256 7•• ••• 101"
}
],
"final_amount_note": "Final loan = delivered quantity × price."
}/evidence/policyGroupPhoto rules for a checkpoint and what is still missing.
| Query | Type | Meaning |
|---|---|---|
checkpoint | string | RECEIPT, LOADED, DROPOFF, CHECKIN_*, WEIGHIN. |
order_id | uuid | The order, to list what is missing. |
Request
GET /api/inputmarket/v1/evidence/policy?checkpoint=RECEIPT&order_id=a32964e9-fe70-4e98-9dde-56d03dd64881
Response
HTTP 200
{
"checkpoint": "RECEIPT",
"policy": {
"actor": "GROUP",
"per_order_line": {
"min": 1
},
"when_rejected_qty_gt_0": {
"slot": "damage_closeup",
"min": 1
},
"optional": [
"group_with_goods"
],
"signature": true,
"gps": true,
"max_distance_m": 300
},
"missing": [
{
"slot": "signature",
"min": 1,
"have": 0
}
]
}/evidenceGroupUploads one photo as multipart/form-data. Idempotency-Key is required. For large files or poor networks use the resumable upload below.
| Body field | Type | Meaning |
|---|---|---|
file | file | JPEG photo. |
subject_type | string | ORDER_RECEIPT for the group. |
subject_id | uuid | Order id. |
slot_code | string | line_photo, damage_closeup, signature, group_with_goods. |
order_line_id optional | uuid | For per-line photos. |
captured_at | datetime | When the photo was taken. |
gps optional | object | {lat, lng} |
captured_offline optional | bool | |
supersedes_id optional | uuid | Replaces an earlier photo. |
Request
POST /api/inputmarket/v1/evidence
Idempotency-Key: 04ae29d5-ba5b-49ba-b075-f07c2a146a81
{
"subject_type": "ORDER_RECEIPT",
"subject_id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"slot_code": "line_photo",
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"captured_at": "2026-11-06T16:19:35Z",
"file": "<file photo.jpg>",
"gps": {
"lat": 0.3601,
"lng": 32.7514
}
}Response
HTTP 201
{
"id": "a32964ea-3c5c-46d4-a0ab-4f12a94461cc",
"subject_type": "ORDER_RECEIPT",
"subject_id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"slot_code": "line_photo",
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"status": "VALID",
"flags": [],
"sha256": "6962d86a401f90a7574aebbe6706bedd766e22202a3a0e2290eb58afc26f883e",
"captured_at": "2026-11-06T16:19:35Z",
"received_at": "2026-11-06T16:19:35Z",
"gps": {
"lat": 0.3601,
"lng": 32.7514
},
"url": "/storage/inputmarket/GROUP/g-100/2026/11/4f46827f-3e21-47be-9982-ed4ac8e8cba4.jpg",
"supersedes_id": null
}/uploadsGroupStarts a resumable upload. Send the same metadata as /evidence plus size_bytes. Idempotency-Key is required. Returns the upload id and chunk_size; the Upload-Offset header starts at 0.
Response
HTTP 201
Upload-Offset: 0
{ "id": "…", "chunk_size": 262144, "offset": 0 }/uploads/{id}GroupSends the next chunk as the raw request body, with header Upload-Offset set to the bytes already sent. Replies 204 with the new Upload-Offset.
/uploads/{id}GroupWhere an upload stands after a lost connection: headers Upload-Offset, Upload-Length, Upload-Status.
/uploads/{id}/completeGroupFinishes the upload and returns the same evidence object as POST /evidence.
/orders/{id}/delivery-confirmation/otpGroupSends a code to the officer confirming.
| Body field | Type | Meaning |
|---|---|---|
officer_id | string |
Request
POST /api/inputmarket/v1/orders/a32964e9-fe70-4e98-9dde-56d03dd64881/delivery-confirmation/otp
Idempotency-Key: 4bd68c91-b103-492d-9e07-1c438f307dae
{
"officer_id": "101"
}Response
HTTP 201
{
"challenge_id": "a32964ea-4324-4586-934f-d6aa794defe1",
"id": "a32964ea-4324-4586-934f-d6aa794defe1",
"masked_phone": "+256 7•• ••• 101",
"expires_at": "2026-11-06T16:29:35Z"
}/orders/{id}/delivery-confirmationsGroupConfirms delivery. Idempotency-Key is required. Quantities refused need a reason_code and a damage photo. Refusing everything opens a dispute instead.
| Body field | Type | Meaning |
|---|---|---|
lines | array | [{order_line_id, qty_received, qty_rejected, reason_code?}] |
photo_ids | array | Evidence ids. |
signature_id | uuid | |
gps | object | {lat, lng} |
receiver optional | object | {name, phone, phone_token}. phone_token from phone checks, unless the phone is the confirming officer's. |
challenge_id, otp | string | The officer's code. |
exception_reason_code optional | string | |
notes optional | string |
Request
POST /api/inputmarket/v1/orders/a32964e9-fe70-4e98-9dde-56d03dd64881/delivery-confirmations
Idempotency-Key: b0acdc3f-f829-4fd9-99cc-0e7175013fc5
{
"receiver": {
"name": "Mary Akello",
"phone": "+256772000101"
},
"lines": [
{
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"qty_received": 10,
"qty_rejected": 0,
"reason_code": null
}
],
"photo_ids": [
"a32964ea-3c5c-46d4-a0ab-4f12a94461cc"
],
"signature_id": "a32964ea-4103-47eb-9807-0637be57af72",
"gps": {
"lat": 0.3601,
"lng": 32.7514
},
"challenge_id": "a32964ea-4324-4586-934f-d6aa794defe1",
"otp": "321259"
}Response
HTTP 201
{
"confirmation_id": "a32964ea-45e2-48a9-9b59-3f4d76c0206d",
"status": "CONFIRMED",
"order_status": "CLOSED",
"payment_status": "PAID",
"final_amount": 1420000,
"wallet_applied": 0,
"loan_amount": 1420000,
"next_steps": "Thank you. The supplier will now be paid and your loan will start."
}/orders/{id}/disputesGroupReports a problem with an order. Ensibuuko reviews it; payment to the supplier waits.
| Body field | Type | Meaning |
|---|---|---|
reason_code | string | One of config.dispute_reasons. |
notes optional | string | |
evidence_ids optional | array |
/orders/{id}/disputesGroupDisputes on an order.
/orders/{id}/evidenceGroupPhotos recorded on an order.
A loan starts when the supplier is paid. Groups see principal, schedule and what they owe; never commission or supplier amounts.
/loansGroupThe group's Marketplace loans, paged.
Request
GET /api/inputmarket/v1/loans
HTTP 200
{
"data": [
{
"id": "a32964ea-4fe5-4e07-b333-4756b3abda9f",
"order_id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"order_ref": "IM-2026-000001",
"provider_name": "Agro Supply",
"principal": 1420000,
"total_payable": 1420000,
"repaid": 0,
"outstanding": 1420000,
"status": "ACTIVE",
"activated_at": "2026-11-06T16:19:35Z",
"due_date": "2027-04-06",
"repayment_mode": "MONTHLY_INSTALMENTS",
"interest_rate_pct": 0,
"schedule": [
{
"due_date": "2026-12-06",
"amount": 284000,
"paid": 0,
"status": "DUE"
}
],
"next_due": {
"due_date": "2026-12-06",
"amount": 284000
}
}
],
"meta": {
"page": 1,
"per_page": 20,
"total": 1
}
}/loans/{id}GroupOne loan with its schedule.
Request
GET /api/inputmarket/v1/loans/a32964ea-4fe5-4e07-b333-4756b3abda9f
HTTP 200
{
"id": "a32964ea-4fe5-4e07-b333-4756b3abda9f",
"order_id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"order_ref": "IM-2026-000001",
"provider_name": "Agro Supply",
"principal": 1420000,
"total_payable": 1420000,
"repaid": 0,
"outstanding": 1420000,
"status": "ACTIVE",
"activated_at": "2026-11-06T16:19:35Z",
"due_date": "2027-04-06",
"repayment_mode": "MONTHLY_INSTALMENTS",
"interest_rate_pct": 0,
"schedule": [
{
"due_date": "2026-12-06",
"amount": 284000,
"paid": 0,
"status": "DUE"
}
],
"next_due": {
"due_date": "2026-12-06",
"amount": 284000
}
}/loans/{id}/repaymentsGroupRepayments received: [{id, amount, channel, reference, received_at}].
Request
GET /api/inputmarket/v1/loans/a32964ea-4fe5-4e07-b333-4756b3abda9f/repayments
Response
HTTP 200
{
"data": []
}/repayments/nextGroupThe next amount due across Marketplace loans and the group's other running loans, with a breakdown.
Request
GET /api/inputmarket/v1/repayments/next
Response
HTTP 200
{
"due_date": "2026-12-06",
"total": 284000,
"breakdown": [
{
"kind": "MARKETPLACE",
"loan_id": "a32964ea-4fe5-4e07-b333-4756b3abda9f",
"label": "Marketplace IM-2026-000001",
"amount": 284000,
"due_date": "2026-12-06"
}
]
}/loans/{id}/repaymentsGroupDirect mobile-money repayment. Off until features.direct_repayment is true; groups repay through the normal Chomoka repayment flow.
| Body field | Type | Meaning |
|---|---|---|
amount | int | |
msisdn | string |
The app works offline and sends queued actions later. Send X-Client-Created-At with the time the action was taken on the phone.
/sync/snapshotGroupEverything that changed since a time: orders, offers, loans, plots, listings, settlements, price proposals, notifications, and tombstones for deleted items. Save server_time and send it as since next time.
| Query | Type | Meaning |
|---|---|---|
since | datetime | Last server_time you saved. Omit for a full snapshot. |
Request
GET /api/inputmarket/v1/sync/snapshot
HTTP 200
{
"since": "1970-01-01T00:00:00Z",
"server_time": "2026-11-06T16:19:35Z",
"orders": [
{
"id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"reference": "IM-2026-000001",
"provider": {
"id": "a32964e9-ce56-46db-bac9-00e337495b2c",
"name": "Agro Supply"
},
"provider_name": "Agro Supply",
"total": 1420000,
"status": {
"fulfilment": "CLOSED",
"payment": "PAID",
"loan": "ACTIVE"
},
"app_status": "CLOSED",
"display_status": {
"label": "Completed",
"tone": "done"
},
"next_action": null,
"created_at": "2026-11-06T16:18:33Z"
}
],
"offers": [
{
"id": "a32964ea-1140-4c26-afe9-fee9d73b823f",
"order_id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"version": 1,
"status": "ACCEPTED",
"expires_at": "2026-11-09T16:18:33Z"
}
],
"loans": [
{
"id": "a32964ea-4fe5-4e07-b333-4756b3abda9f",
"order_id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"order_ref": "IM-2026-000001",
"provider_name": "Agro Supply",
"principal": 1420000,
"total_payable": 1420000,
"repaid": 0,
"outstanding": 1420000,
"status": "ACTIVE",
"activated_at": "2026-11-06T16:19:35Z",
"due_date": "2027-04-06",
"repayment_mode": "MONTHLY_INSTALMENTS",
"interest_rate_pct": 0,
"schedule": [
{
"due_date": "2026-12-06",
"amount": 284000,
"paid": 0,
"status": "DUE"
}
],
"next_due": {
"due_date": "2026-12-06",
"amount": 284000
}
}
],
"plots": [],
"listings": [],
"settlements": [],
"price_proposals": [],
"tombstones": [],
"notifications": [
{
"id": "a32964ea-20f7-4064-98f4-8af48c22b808",
"type": "OFFER_ACCEPTED",
"title": "Order confirmed",
"body": "Order IM-2026-000001 is confirmed. The supplier will prepare it.",
"payload": {
"order_id": "a32964e9-fe70-4e98-9dde-56d03dd64881"
},
"read_at": null,
"created_at": "2026-11-06T16:19:35Z"
}
]
}An Agent helping the group signs in on the group's phone with their Agent code and a code to their own phone. The token is held in memory only, lasts config.agent.session_minutes (30 by default), and is sent as X-Agent-Authority on every request the Agent makes. Agent steps must be live: an action whose X-Client-Created-At is too old is refused with AGENT_ACTION_REQUIRES_LIVE.
/agent/challengeGroupStarts sign-in. identifier is the Agent code or the Agent's phone.
| Body field | Type | Meaning |
|---|---|---|
identifier | string | Agent code or phone. |
Request
POST /api/inputmarket/v1/agent/challenge
Idempotency-Key: e1280fb5-9213-4f4e-943e-c1bb67420759
{
"identifier": "DCE-0001"
}Response
HTTP 202
{
"challenge_id": "a32964ea-5f80-4454-ad74-182a51cdf45e",
"masked_phone": "+256 7•• ••• 001",
"expires_at": "2026-11-06T16:29:35Z",
"agent": {
"id": "agent-1",
"name": "Joan K."
},
"id": "a32964ea-5f80-4454-ad74-182a51cdf45e"
}/agent/verifyGroupReturns the authority_token and what the Agent may do (capabilities).
| Body field | Type | Meaning |
|---|---|---|
challenge_id | uuid | |
otp | string |
Request
POST /api/inputmarket/v1/agent/verify
Idempotency-Key: a657ddd9-8a94-4bb2-bac5-94d50ce6882e
{
"challenge_id": "a32964ea-5f80-4454-ad74-182a51cdf45e",
"otp": "092396"
}Response
HTTP 200
{
"authority_token": "nzbOfZtKYJfJxTEp9QCdl1qsF8zDFW3mU5zFKTc9BVqM2mkNl4uD1mr104hhfyoJ",
"authority_id": "a32964ea-6091-45ec-8753-01012c31be62",
"expires_at": "2026-11-06T16:49:35Z",
"agent": {
"id": "agent-1",
"name": "Joan K."
},
"capabilities": [
"consent.collect"
]
}/agent/sessionGroup + AgentEnds the Agent session. Accepted even after the token expired.
Request
DELETE /api/inputmarket/v1/agent/session
Idempotency-Key: 98285938-b2d7-4f8c-a444-ae8b38cd2cb0
X-Agent-Authority: <authority_token>
Response
HTTP 204Members' plots and season plans, used for loans, check-ins and harvest sales.
/seasonsGroupSeasons.
Request
GET /api/inputmarket/v1/seasons
Response
HTTP 200
{
"data": [
{
"id": "a32964ea-6866-481f-993f-4cab0149dc19",
"name": "Season B 2026",
"starts_on": "2026-10-06",
"ends_on": "2027-04-06"
}
]
}/farm/plotsGroupPlots, optionally for one member.
| Query | Type | Meaning |
|---|---|---|
member_id | string | Only this member. |
Request
GET /api/inputmarket/v1/farm/plots
Response
HTTP 200
{
"data": [
{
"id": "a32964ea-6ac0-4f16-8e08-0d63eaaf60ca",
"member_id": "104",
"name": "Main plot",
"area_declared_acres": 2,
"area_measured_acres": null,
"measurement_method": null,
"tenure": null,
"geo_point": {
"lat": 0.3601,
"lng": 32.7514
},
"boundary": null,
"gps_accuracy_m": null,
"answers": [],
"form_version": null,
"status": "COMPLETE",
"flags": [],
"updated_at": "2026-11-06T16:19:35Z"
}
]
}/farm/plotsGroupAdds a plot.
| Body field | Type | Meaning |
|---|---|---|
member_id | string | |
name | string | |
area_declared_acres optional | number | |
geo_point optional | object | {lat, lng} |
tenure optional | string | |
answers optional | object | Form answers. |
Request
POST /api/inputmarket/v1/farm/plots
Idempotency-Key: 8bd383d4-73bc-4db8-8cc3-2c878ba55218
{
"member_id": "104",
"name": "Main plot",
"area_declared_acres": 2,
"geo_point": {
"lat": 0.3601,
"lng": 32.7514
}
}Response
HTTP 201
{
"id": "a32964ea-6ac0-4f16-8e08-0d63eaaf60ca",
"member_id": "104",
"name": "Main plot",
"area_declared_acres": 2,
"area_measured_acres": null,
"measurement_method": null,
"tenure": null,
"geo_point": {
"lat": 0.3601,
"lng": 32.7514
},
"boundary": null,
"gps_accuracy_m": null,
"answers": [],
"form_version": null,
"status": "COMPLETE",
"flags": [],
"updated_at": "2026-11-06T16:19:35Z"
}/farm/plots/{id}GroupUpdates a plot.
/farm/plots/{id}/boundaryGroupSaves a walked boundary as a GeoJSON Polygon. Area is measured and compared with the declared area.
| Body field | Type | Meaning |
|---|---|---|
boundary | GeoJSON | Polygon. |
gps_accuracy_m optional | number |
/farm/summaryGroupHow complete the group's farm records are.
Request
GET /api/inputmarket/v1/farm/summary
Response
HTTP 200
{
"completeness_pct": 16,
"plots_count": 1,
"complete_plots": 1,
"flagged_plots": 0,
"members_with_plots": 1,
"members_total": 6,
"total_declared_acres": 2,
"total_measured_acres": 0,
"expected_kg": 0,
"season": {
"id": "a32964ea-6866-481f-993f-4cab0149dc19",
"name": "Season B 2026"
}
}/farm/plots/{id}/seasons/{seasonId}GroupA plot's plan for a season (crop, variety, planting date, expected yield).
/farm/plots/{id}/seasons/{seasonId}GroupSaves that plan.
/farm/checkins/dueGroupField check-ins due.
/farm/checkinsGroupSubmits a check-in with photos.
| Body field | Type | Meaning |
|---|---|---|
plot_season_id | uuid | |
stage | string | PLANTING, GERMINATION, MIDSEASON, PREHARVEST. |
evidence_ids optional | array | |
agent_present optional | bool |
/forms/{key}GroupA dynamic form definition (questions for plots and seasons).
The group lists harvest, members pledge with consent, the group accepts an offtaker's offer, officers sign the commitment, the group accepts a price, and after the weigh-in it confirms the weight and then the money.
/offtake/forecastGroupExpected harvest from the farm records.
/offtake/listingsGroupThe group's listings.
/offtake/listingsGroupCreates a draft listing.
| Body field | Type | Meaning |
|---|---|---|
crop | string | |
qty_min_kg, qty_max_kg | number | |
season_id optional | uuid | |
ready_from, ready_to optional | date | |
quality_spec optional | object | |
pickup_location optional | object |
Request
POST /api/inputmarket/v1/offtake/listings
Idempotency-Key: dbdf731a-f6a9-458e-ae17-94f6e39eab42
{
"crop": "Maize",
"qty_min_kg": 5000,
"qty_max_kg": 9000,
"season_id": "a32964ea-6866-481f-993f-4cab0149dc19",
"ready_from": "2026-12-06"
}Response
HTTP 201
{
"id": "a32964ea-6eb3-475f-ba5c-addc48e7cffb",
"crop": "Maize",
"season_id": "a32964ea-6866-481f-993f-4cab0149dc19",
"qty_min_kg": 5000,
"qty_max_kg": 9000,
"pledged_kg": 0,
"quality_spec": null,
"ready_from": "2026-12-06",
"ready_to": null,
"pickup_location": null,
"open_to": null,
"linked_buyer_id": null,
"status": "DRAFT"
}/offtake/listings/{id}GroupUpdates a draft listing.
/offtake/listings/{id}/publishGroupShows the listing to offtakers.
Request
POST /api/inputmarket/v1/offtake/listings/a32964ea-6eb3-475f-ba5c-addc48e7cffb/publish
Idempotency-Key: 70e89676-c52a-45b0-911c-d4b1df0206f0
Response
HTTP 200
{
"id": "a32964ea-6eb3-475f-ba5c-addc48e7cffb",
"crop": "Maize",
"season_id": "a32964ea-6866-481f-993f-4cab0149dc19",
"qty_min_kg": 5000,
"qty_max_kg": 9000,
"pledged_kg": 0,
"quality_spec": null,
"ready_from": "2026-12-06",
"ready_to": null,
"pickup_location": null,
"open_to": "APPROVED_BUYERS",
"linked_buyer_id": null,
"status": "PUBLISHED"
}/offtake/listings/{id}/pledgesGroupA member pledges kilograms, with a PLEDGE consent.
| Body field | Type | Meaning |
|---|---|---|
member_id | string | |
qty_kg | number | |
consent_id | uuid | From /consents/verify. |
Request
POST /api/inputmarket/v1/offtake/listings/a32964ea-6eb3-475f-ba5c-addc48e7cffb/pledges
Idempotency-Key: 9b46815c-427d-4952-81e3-6cda20316bda
{
"member_id": "104",
"qty_kg": 4000,
"consent_id": "a32964ea-7392-4f35-98c2-f4cf325474d7"
}Response
HTTP 201
{
"data": [
{
"id": "a32964ea-7523-420d-8565-d4d695b5702c",
"member_id": "104",
"qty_kg": 4000,
"consent_id": "a32964ea-7392-4f35-98c2-f4cf325474d7"
}
]
}/offtake/listings/{id}/offersGroupOffers from offtakers.
Request
GET /api/inputmarket/v1/offtake/listings/a32964ea-6eb3-475f-ba5c-addc48e7cffb/offers
Response
HTTP 200
{
"data": [
{
"id": "a32964ea-77bd-4df0-ac6c-b0dcf0814fa9",
"buyer": {
"id": "a32964ea-6ce9-4e2a-ac8e-1b35f899c6c8",
"name": "Grain Buyer Ltd"
},
"version": 1,
"qty_max_kg": 4000,
"pickup_from": "2026-12-06",
"pickup_to": null,
"payment_terms_hours": 48,
"quality_spec": null,
"conditions": null,
"expires_at": "2026-11-09T16:20:06Z",
"status": "OPEN"
}
]
}/offtake/offers/{id}/acceptGroupAccepts an offer and creates a draft commitment. Other open offers are declined.
Request
POST /api/inputmarket/v1/offtake/offers/a32964ea-77bd-4df0-ac6c-b0dcf0814fa9/accept
Idempotency-Key: 469f6ef3-9943-49b1-bccc-6fe3e0249aeb
HTTP 201
{
"id": "a32964ea-7a8d-478f-983f-ac7c46b23737",
"listing_id": "a32964ea-6eb3-475f-ba5c-addc48e7cffb",
"buyer": {
"id": "a32964ea-6ce9-4e2a-ac8e-1b35f899c6c8",
"name": "Grain Buyer Ltd"
},
"terms_version": 1,
"terms": {
"version": 1,
"buyer": {
"id": "a32964ea-6ce9-4e2a-ac8e-1b35f899c6c8",
"name": "Grain Buyer Ltd"
},
"crop": "Maize",
"qty_kg": 4000,
"tolerance_pct": 10,
"pickup_from": "2026-12-06",
"pickup_to": null,
"payment_terms_hours": 48,
"quality": null,
"price": "Set by the buyer before pickup. The group accepts or declines it with an officer code.",
"loans": "Selling the harvest does not change what the group owes on any loan.",
"blocks": [
{
"title": "What the group sells",
"body": "Maize, about 4,000 kg (±10%)."
}
]
},
"status": "DRAFT",
"binding_at": null,
"signatures": []
}/offtake/offers/{id}/declineGroupDeclines an offer.
/offtake/commitments/{id}GroupA commitment with its terms and signatures.
Request
GET /api/inputmarket/v1/offtake/commitments/a32964ea-7a8d-478f-983f-ac7c46b23737
HTTP 200
{
"id": "a32964ea-7a8d-478f-983f-ac7c46b23737",
"listing_id": "a32964ea-6eb3-475f-ba5c-addc48e7cffb",
"buyer": {
"id": "a32964ea-6ce9-4e2a-ac8e-1b35f899c6c8",
"name": "Grain Buyer Ltd"
},
"terms_version": 1,
"terms": {
"version": 1,
"buyer": {
"id": "a32964ea-6ce9-4e2a-ac8e-1b35f899c6c8",
"name": "Grain Buyer Ltd"
},
"crop": "Maize",
"qty_kg": 4000,
"tolerance_pct": 10,
"pickup_from": "2026-12-06",
"pickup_to": null,
"payment_terms_hours": 48,
"quality": null,
"price": "Set by the buyer before pickup. The group accepts or declines it with an officer code.",
"loans": "Selling the harvest does not change what the group owes on any loan.",
"blocks": [
{
"title": "What the group sells",
"body": "Maize, about 4,000 kg (±10%)."
}
]
},
"status": "BINDING",
"binding_at": "2026-11-06T16:20:06Z",
"signatures": [
{
"party": "GROUP_OFFICER",
"signer_id": "101",
"terms_version": 1,
"signed_at": "2026-11-06T16:20:06Z",
"current": true
}
]
}/offtake/commitments/{id}/otpGroupSends a code to an officer.
| Body field | Type | Meaning |
|---|---|---|
officer_id | string |
Request
POST /api/inputmarket/v1/offtake/commitments/a32964ea-7a8d-478f-983f-ac7c46b23737/otp
Idempotency-Key: f76396b8-538e-4edb-af6c-311711aa27ad
{
"officer_id": "101"
}Response
HTTP 201
{
"challenge_id": "a32964ea-7c8b-4eb1-a465-6406812c5183",
"masked_phone": "+256 7•• ••• 101",
"expires_at": "2026-11-06T16:30:06Z"
}/offtake/commitments/{id}/signGroupGroup + AgentAn officer signs with a code; an Agent signs as witness with only the agent header. Binding once officers, the offtaker and the Agent (when required) have signed.
| Body field | Type | Meaning |
|---|---|---|
challenge_id, otp | string | Officer's code (not needed for the Agent witness). |
terms_version | int | The version shown. |
Request
POST /api/inputmarket/v1/offtake/commitments/a32964ea-7a8d-478f-983f-ac7c46b23737/sign
Idempotency-Key: b19bb495-1c10-4bb3-a67c-321bc3723a62
X-Agent-Authority: <authority_token>
{
"challenge_id": "a32964ea-7c8b-4eb1-a465-6406812c5183",
"otp": "228783",
"terms_version": 1
}HTTP 200
{
"id": "a32964ea-7a8d-478f-983f-ac7c46b23737",
"listing_id": "a32964ea-6eb3-475f-ba5c-addc48e7cffb",
"buyer": {
"id": "a32964ea-6ce9-4e2a-ac8e-1b35f899c6c8",
"name": "Grain Buyer Ltd"
},
"terms_version": 1,
"terms": {
"version": 1,
"buyer": {
"id": "a32964ea-6ce9-4e2a-ac8e-1b35f899c6c8",
"name": "Grain Buyer Ltd"
},
"crop": "Maize",
"qty_kg": 4000,
"tolerance_pct": 10,
"pickup_from": "2026-12-06",
"pickup_to": null,
"payment_terms_hours": 48,
"quality": null,
"price": "Set by the buyer before pickup. The group accepts or declines it with an officer code.",
"loans": "Selling the harvest does not change what the group owes on any loan.",
"blocks": [
{
"title": "What the group sells",
"body": "Maize, about 4,000 kg (±10%)."
}
]
},
"status": "PARTIALLY_SIGNED",
"binding_at": null,
"signatures": [
{
"party": "GROUP_OFFICER",
"signer_id": "101",
"terms_version": 1,
"signed_at": "2026-11-06T16:20:06Z",
"current": true
}
]
}/offtake/price-proposalsGroupPrices offered by offtakers.
Request
GET /api/inputmarket/v1/offtake/price-proposals
Response
HTTP 200
{
"data": [
{
"id": "a32964ea-8b75-4a07-b1c4-cf925e733ba7",
"listing_id": "a32964ea-6eb3-475f-ba5c-addc48e7cffb",
"commitment_id": "a32964ea-7a8d-478f-983f-ac7c46b23737",
"buyer": {
"id": "a32964ea-6ce9-4e2a-ac8e-1b35f899c6c8",
"name": "Grain Buyer Ltd"
},
"mode": "AT_OFFTAKE",
"price_per_kg": 1200,
"valid_until": "2026-11-07T16:20:06Z",
"quality_terms": null,
"status": "OPEN",
"accepted_at": null
}
]
}/offtake/price-proposals/{id}/otpGroupCode for the officer accepting a price.
| Body field | Type | Meaning |
|---|---|---|
officer_id | string |
/offtake/price-proposals/{id}/acceptGroupAccepts the price.
| Body field | Type | Meaning |
|---|---|---|
challenge_id, otp | string |
Request
POST /api/inputmarket/v1/offtake/price-proposals/a32964ea-8b75-4a07-b1c4-cf925e733ba7/accept
Idempotency-Key: 33abff54-09ef-45ea-a1f2-ee2c8b6e63cc
{
"challenge_id": "a32964ea-8e0d-4613-92f3-6be86aa8c3fb",
"otp": "621355"
}Response
HTTP 200
{
"id": "a32964ea-8b75-4a07-b1c4-cf925e733ba7",
"listing_id": "a32964ea-6eb3-475f-ba5c-addc48e7cffb",
"commitment_id": "a32964ea-7a8d-478f-983f-ac7c46b23737",
"buyer": {
"id": "a32964ea-6ce9-4e2a-ac8e-1b35f899c6c8",
"name": "Grain Buyer Ltd"
},
"mode": "AT_OFFTAKE",
"price_per_kg": 1200,
"valid_until": "2026-11-07T16:20:06Z",
"quality_terms": null,
"status": "ACCEPTED",
"accepted_at": "2026-11-06T16:20:06Z"
}/offtake/price-proposals/{id}/declineGroupDeclines the price.
/offtake/pickupsGroupPickups booked by the offtaker.
Request
GET /api/inputmarket/v1/offtake/pickups
Response
HTTP 200
{
"data": [
{
"id": "a32964ea-9198-4943-ae3d-4c312c36ee67",
"commitment_id": "a32964ea-7a8d-478f-983f-ac7c46b23737",
"scheduled_at": "2026-11-07T16:20:06Z",
"vehicle": "UBF 456K",
"rep": {
"name": "Peter",
"phone": "+256772000777"
},
"location": null,
"ready_info": null,
"status": "SCHEDULED"
}
]
}/offtake/pickups/{id}/readyGroupThe group says the harvest is ready.
| Body field | Type | Meaning |
|---|---|---|
actual_qty_kg optional | number | |
moisture_pct optional | number | |
evidence_ids optional | array |
/offtake/weigh-ins/{id}GroupA weigh ticket.
/offtake/weigh-ins/{id}/otpGroupCode for the officer confirming the weight.
| Body field | Type | Meaning |
|---|---|---|
officer_id | string |
/offtake/weigh-ins/{id}/confirmGroupConfirms the weight. With a price accepted, the settlement is calculated (settlement_id).
| Body field | Type | Meaning |
|---|---|---|
challenge_id, otp | string |
Request
POST /api/inputmarket/v1/offtake/weigh-ins/a32964ea-94c3-43d1-b208-3c79f828d8c7/confirm
Idempotency-Key: 4201bd44-df2d-4e0e-80b3-80f1375a8407
{
"challenge_id": "a32964ea-97a4-4115-81e3-c205596d4c88",
"otp": "273999"
}HTTP 200
{
"id": "a32964ea-94c3-43d1-b208-3c79f828d8c7",
"pickup_id": "a32964ea-9198-4943-ae3d-4c312c36ee67",
"ticket_no": "T-1",
"gross_kg": 4100,
"tare_kg": 100,
"net_kg": 4000,
"moisture_pct": 12.5,
"grade": "Grade 1",
"deductions": [],
"per_member": [
{
"member_id": "104",
"kg": 4000
}
],
"evidence_ids": [],
"confirmations": [
{
"party": "BUYER_REP",
"user_id": "a32964ea-6d0f-4efb-9d8a-d67fb46d93f2",
"at": "2026-11-06T16:20:06Z",
"otp": false
}
],
"status": "CONFIRMED",
"supersedes_id": null,
"settlement_id": "a32964ea-9a51-4d55-ad34-add04838b0b5"
}/offtake/weigh-ins/{id}/disputeGroupDisputes the weight; the offtaker must re-weigh.
| Body field | Type | Meaning |
|---|---|---|
notes optional | string |
/offtake/settlements/{id}GroupWhat the group is owed: net kg × price, deductions, each member's share.
Request
GET /api/inputmarket/v1/offtake/settlements/a32964ea-9a51-4d55-ad34-add04838b0b5
Response
HTTP 200
{
"id": "a32964ea-9a51-4d55-ad34-add04838b0b5",
"weigh_in_id": "a32964ea-94c3-43d1-b208-3c79f828d8c7",
"price_per_kg": 1200,
"gross_value": 4800000,
"other_deductions": [],
"net_to_group": 4800000,
"per_member": [
{
"member_id": "104",
"kg": 4000,
"gross_share": 4800000,
"net_share": 4800000
}
],
"status": "COMPUTED",
"balance_due_at": "2026-11-08T16:20:06Z",
"paid_to_group_at": null,
"payment_reference": null,
"group_confirmed_at": null
}/offtake/settlements/{id}/confirm-receivedGroupThe group confirms the money arrived. The sale is complete.
Request
POST /api/inputmarket/v1/offtake/settlements/a32964ea-9a51-4d55-ad34-add04838b0b5/confirm-received
Idempotency-Key: 93349dc5-cea3-4f0c-88fd-45e44c8898c5
Response
HTTP 200
{
"id": "a32964ea-9a51-4d55-ad34-add04838b0b5",
"weigh_in_id": "a32964ea-94c3-43d1-b208-3c79f828d8c7",
"price_per_kg": 1200,
"gross_value": 4800000,
"other_deductions": [],
"net_to_group": 4800000,
"per_member": [
{
"member_id": "104",
"kg": 4000,
"gross_share": 4800000,
"net_share": 4800000
}
],
"status": "GROUP_CONFIRMED",
"balance_due_at": "2026-11-08T16:20:06Z",
"paid_to_group_at": "2026-11-06T16:20:06Z",
"payment_reference": "MM-123",
"group_confirmed_at": "2026-11-06T16:20:06Z"
}For suppliers connecting their own systems. Everything here is also available on the supplier web portal at market.chomoka.org/login. Prices here are the supplier's own (unit_provider_price); the supplier never sees Ensibuuko's fee.
/provider/auth/otpNo authSends a sign-in code to a phone Ensibuuko registered for the company.
| Body field | Type | Meaning |
|---|---|---|
phone | string | International (+256…) or local form. |
provider_id optional | uuid | When one phone belongs to several companies. |
Request
POST /api/inputmarket/v1/provider/auth/otp
{
"phone": "+256772000555"
}Response
HTTP 202
{
"challenge_id": "a32964ea-0919-46d4-bae4-417064ef2340",
"masked_phone": "+256 7•• ••• 555",
"expires_at": "2026-11-06T16:28:33Z"
}/provider/auth/verifyNo authReturns a bearer token valid for 30 days. Send it as Authorization: Bearer <token>.
| Body field | Type | Meaning |
|---|---|---|
challenge_id | uuid | |
otp | string |
Request
POST /api/inputmarket/v1/provider/auth/verify
{
"challenge_id": "a32964ea-0919-46d4-bae4-417064ef2340",
"otp": "607883"
}Response
HTTP 200
{
"token": "TEOLowo52zKgxoHv0hzDgoQDZqYNbpXvl1xyS8Ep0fcXPYu9Wu8nuG622ZUOyeb5",
"expires_at": "2026-12-06T16:18:33Z",
"user": {
"id": "a32964ea-0881-4469-a852-6d7bea31ef6d",
"name": "Sales desk",
"role": "ADMIN"
},
"provider": {
"id": "a32964e9-ce56-46db-bac9-00e337495b2c",
"name": "Agro Supply",
"kind": "BOTH"
}
}/provider/auth/logoutSupplierRevokes the token.
/provider/ordersSupplierOrders for the company, paged, with the group's delivery place and contacts.
Request
GET /api/inputmarket/v1/provider/orders
HTTP 200
{
"data": [
{
"id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"reference": "IM-2026-000001",
"status": {
"fulfilment": "SUBMITTED",
"payment": "NOT_DUE"
},
"group": {
"id": "g-100"
},
"delivery": {
"use_group_location": true,
"location_id": null,
"hierarchy": {
"region": "Central",
"district": "Mukono",
"subcounty": "Goma",
"parish": "Seeta",
"village": "Bajjo"
},
"geo": {
"lat": 0.3601,
"lng": 32.7514
},
"landmark": "Near the church",
"address": null,
"preferred_window": "Next week",
"receiver": {
"name": "Mary Akello",
"phone": "+256772000101"
}
},
"contacts": [
{
"kind": "ACCOUNT_OWNER",
"name": "Mary Akello",
"role": "CHAIRPERSON",
"phone": "+256772000101",
"member_no": "101",
"verified": true
}
],
"lines": [
{
"id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"product_id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"name": "Maize seed (Longe 5)",
"unit": "kg",
"unit_provider_price": 100000,
"qty_ordered": 10,
"qty_confirmed": null,
"qty_dispatched": 0,
"qty_delivered": 0,
"qty_received": 0,
"qty_rejected": 0,
"line_provider_total": 1000000
}
],
"provider_total": 1360000,
"sla_due_at": "2026-11-07T16:18:33Z",
"offer_expires_at": null,
"dispatches": [],
"deliveries": [],
"payment": null,
"created_at": "2026-11-06T16:18:33Z"
}
],
"meta": {
"page": 1,
"per_page": 20,
"total": 1
}
}/provider/orders/{id}SupplierOne order. Opening a new order marks it as being reviewed.
Request
GET /api/inputmarket/v1/provider/orders/a32964e9-fe70-4e98-9dde-56d03dd64881
HTTP 200
{
"id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"reference": "IM-2026-000001",
"status": {
"fulfilment": "PROVIDER_REVIEW",
"payment": "NOT_DUE"
},
"group": {
"id": "g-100"
},
"delivery": {
"use_group_location": true,
"location_id": null,
"hierarchy": {
"region": "Central",
"district": "Mukono",
"subcounty": "Goma",
"parish": "Seeta",
"village": "Bajjo"
},
"geo": {
"lat": 0.3601,
"lng": 32.7514
},
"landmark": "Near the church",
"address": null,
"preferred_window": "Next week",
"receiver": {
"name": "Mary Akello",
"phone": "+256772000101"
}
},
"contacts": [
{
"kind": "ACCOUNT_OWNER",
"name": "Mary Akello",
"role": "CHAIRPERSON",
"phone": "+256772000101",
"member_no": "101",
"verified": true
}
],
"lines": [
{
"id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"product_id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"name": "Maize seed (Longe 5)",
"unit": "kg",
"unit_provider_price": 100000,
"qty_ordered": 10,
"qty_confirmed": null,
"qty_dispatched": 0,
"qty_delivered": 0,
"qty_received": 0,
"qty_rejected": 0,
"line_provider_total": 1000000
}
],
"provider_total": 1360000,
"sla_due_at": "2026-11-07T16:18:33Z",
"offer_expires_at": null,
"dispatches": [],
"deliveries": [],
"payment": null,
"created_at": "2026-11-06T16:18:33Z"
}/provider/orders/{id}/respondSupplierAnswers a new order before sla_due_at.
| Body field | Type | Meaning |
|---|---|---|
decision | string | ACCEPT, PARTIAL or REJECT. |
lines optional | array | For PARTIAL: [{order_line_id, qty_confirmed}]. |
reason optional | string | Required for REJECT. |
Request
POST /api/inputmarket/v1/provider/orders/a32964e9-fe70-4e98-9dde-56d03dd64881/respond
{
"decision": "ACCEPT",
"lines": []
}HTTP 200
{
"id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"reference": "IM-2026-000001",
"status": {
"fulfilment": "OFFER_READY",
"payment": "NOT_DUE"
},
"group": {
"id": "g-100"
},
"delivery": {
"use_group_location": true,
"location_id": null,
"hierarchy": {
"region": "Central",
"district": "Mukono",
"subcounty": "Goma",
"parish": "Seeta",
"village": "Bajjo"
},
"geo": {
"lat": 0.3601,
"lng": 32.7514
},
"landmark": "Near the church",
"address": null,
"preferred_window": "Next week",
"receiver": {
"name": "Mary Akello",
"phone": "+256772000101"
}
},
"contacts": [
{
"kind": "ACCOUNT_OWNER",
"name": "Mary Akello",
"role": "CHAIRPERSON",
"phone": "+256772000101",
"member_no": "101",
"verified": true
}
],
"lines": [
{
"id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"product_id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"name": "Maize seed (Longe 5)",
"unit": "kg",
"unit_provider_price": 100000,
"qty_ordered": 10,
"qty_confirmed": 10,
"qty_dispatched": 0,
"qty_delivered": 0,
"qty_received": 0,
"qty_rejected": 0,
"line_provider_total": 1000000
}
],
"provider_total": 1360000,
"sla_due_at": "2026-11-07T16:18:33Z",
"offer_expires_at": "2026-11-09T16:18:33Z",
"dispatches": [],
"deliveries": [],
"payment": null,
"created_at": "2026-11-06T16:18:33Z"
}/provider/orders/{id}/statusSupplierMoves an order to PREPARING, or CANCELLED with a reason.
| Body field | Type | Meaning |
|---|---|---|
status | string | PREPARING or CANCELLED. |
reason optional | string |
Request
POST /api/inputmarket/v1/provider/orders/a32964e9-fe70-4e98-9dde-56d03dd64881/status
{
"status": "PREPARING"
}HTTP 200
{
"id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"reference": "IM-2026-000001",
"status": {
"fulfilment": "PREPARING",
"payment": "NOT_DUE"
},
"group": {
"id": "g-100"
},
"delivery": {
"use_group_location": true,
"location_id": null,
"hierarchy": {
"region": "Central",
"district": "Mukono",
"subcounty": "Goma",
"parish": "Seeta",
"village": "Bajjo"
},
"geo": {
"lat": 0.3601,
"lng": 32.7514
},
"landmark": "Near the church",
"address": null,
"preferred_window": "Next week",
"receiver": {
"name": "Mary Akello",
"phone": "+256772000101"
}
},
"contacts": [
{
"kind": "ACCOUNT_OWNER",
"name": "Mary Akello",
"role": "CHAIRPERSON",
"phone": "+256772000101",
"member_no": "101",
"verified": true
}
],
"lines": [
{
"id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"product_id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"name": "Maize seed (Longe 5)",
"unit": "kg",
"unit_provider_price": 100000,
"qty_ordered": 10,
"qty_confirmed": 10,
"qty_dispatched": 0,
"qty_delivered": 0,
"qty_received": 0,
"qty_rejected": 0,
"line_provider_total": 1000000
}
],
"provider_total": 1360000,
"sla_due_at": "2026-11-07T16:18:33Z",
"offer_expires_at": "2026-11-09T16:18:33Z",
"dispatches": [],
"deliveries": [],
"payment": null,
"created_at": "2026-11-06T16:18:33Z"
}/provider/evidenceSupplierUploads a loading or drop-off photo (multipart). Same fields as the group's /evidence, with subject_type ORDER_LOADED (slots goods, vehicle_plate) or ORDER_DROPOFF (slot goods_at_site, with GPS). Resumable uploads work the same under /provider/uploads.
Request
POST /api/inputmarket/v1/provider/evidence
{
"subject_type": "ORDER_DROPOFF",
"subject_id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"slot_code": "goods_at_site",
"captured_at": "2026-11-06T16:19:35Z",
"file": "<file photo.jpg>",
"gps": {
"lat": 0.3601,
"lng": 32.7514
}
}Response
HTTP 201
{
"id": "a32964ea-3328-4f87-9fef-4394725e4552",
"subject_type": "ORDER_DROPOFF",
"subject_id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"slot_code": "goods_at_site",
"order_line_id": null,
"status": "VALID",
"flags": [],
"sha256": "7876cba3c892d917245131be2e927c4989a45709e6cb7b58e5ac4f9d7f73203d",
"captured_at": "2026-11-06T16:19:35Z",
"received_at": "2026-11-06T16:19:35Z",
"gps": {
"lat": 0.3601,
"lng": 32.7514
},
"url": "/storage/inputmarket/PROVIDER/a32964e9-ce56-46db-bac9-00e337495b2c/2026/11/88dd00cb-ab4b-4f7b-bac3-fa92277712f5.jpg",
"supersedes_id": null
}/provider/orders/{id}/dispatchesSupplierRecords goods leaving. Needs the loading photos first. Without lines, everything confirmed is dispatched.
| Body field | Type | Meaning |
|---|---|---|
carrier | object | {name, phone, phone_token, vehicle}. A phone needs phone_token from /provider/phone-checks; the saved name is the confirmed one. |
lines optional | array | [{order_line_id, qty}] |
eta_from, eta_to optional | datetime | |
notes optional | string |
Request
POST /api/inputmarket/v1/provider/orders/a32964e9-fe70-4e98-9dde-56d03dd64881/dispatches
{
"carrier": {
"name": "Driver",
"phone": "+256700000000",
"vehicle": "UAX 123B"
}
}HTTP 201
{
"id": "a32964ea-3019-489d-a64c-96ff8c9182e2",
"lines": [
{
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"qty": 10
}
],
"carrier": {
"name": "Driver",
"phone": "+256700000000",
"vehicle": "UAX 123B"
},
"dispatched_at": "2026-11-06T16:19:35Z",
"eta_from": null,
"eta_to": null,
"notes": null,
"order": {
"id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"reference": "IM-2026-000001",
"status": {
"fulfilment": "DISPATCHED",
"payment": "NOT_DUE"
},
"group": {
"id": "g-100"
},
"delivery": {
"use_group_location": true,
"location_id": null,
"hierarchy": {
"region": "Central",
"district": "Mukono",
"subcounty": "Goma",
"parish": "Seeta",
"village": "Bajjo"
},
"geo": {
"lat": 0.3601,
"lng": 32.7514
},
"landmark": "Near the church",
"address": null,
"preferred_window": "Next week",
"receiver": {
"name": "Mary Akello",
"phone": "+256772000101"
}
},
"contacts": [
{
"kind": "ACCOUNT_OWNER",
"name": "Mary Akello",
"role": "CHAIRPERSON",
"phone": "+256772000101",
"member_no": "101",
"verified": true
}
],
"lines": [
{
"id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"product_id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"name": "Maize seed (Longe 5)",
"unit": "kg",
"unit_provider_price": 100000,
"qty_ordered": 10,
"qty_confirmed": 10,
"qty_dispatched": 10,
"qty_delivered": 0,
"qty_received": 0,
"qty_rejected": 0,
"line_provider_total": 1000000
}
],
"provider_total": 1360000,
"sla_due_at": "2026-11-07T16:18:33Z",
"offer_expires_at": "2026-11-09T16:18:33Z",
"dispatches": [
{
"id": "a32964ea-3019-489d-a64c-96ff8c9182e2",
"lines": [
{
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"qty": 10
}
],
"carrier": {
"name": "Driver",
"phone": "+256700000000",
"vehicle": "UAX 123B"
},
"dispatched_at": "2026-11-06T16:19:35Z",
"eta_from": null,
"eta_to": null,
"notes": null
}
],
"deliveries": [],
"payment": null,
"created_at": "2026-11-06T16:18:33Z"
}
}/provider/orders/{id}/deliveriesSupplierRecords goods handed over. Needs the drop-off photo. The group then confirms in the app.
| Body field | Type | Meaning |
|---|---|---|
delivered_to_name optional | string | |
lines optional | array | |
delivered_at optional | datetime | |
gps optional | object |
Request
POST /api/inputmarket/v1/provider/orders/a32964e9-fe70-4e98-9dde-56d03dd64881/deliveries
{
"delivered_to_name": "Mary"
}HTTP 201
{
"id": "a32964ea-3556-4312-a653-47054d8ad35b",
"order": {
"id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"reference": "IM-2026-000001",
"status": {
"fulfilment": "AWAITING_GROUP_CONFIRMATION",
"payment": "NOT_DUE"
},
"group": {
"id": "g-100"
},
"delivery": {
"use_group_location": true,
"location_id": null,
"hierarchy": {
"region": "Central",
"district": "Mukono",
"subcounty": "Goma",
"parish": "Seeta",
"village": "Bajjo"
},
"geo": {
"lat": 0.3601,
"lng": 32.7514
},
"landmark": "Near the church",
"address": null,
"preferred_window": "Next week",
"receiver": {
"name": "Mary Akello",
"phone": "+256772000101"
}
},
"contacts": [
{
"kind": "ACCOUNT_OWNER",
"name": "Mary Akello",
"role": "CHAIRPERSON",
"phone": "+256772000101",
"member_no": "101",
"verified": true
}
],
"lines": [
{
"id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"product_id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"name": "Maize seed (Longe 5)",
"unit": "kg",
"unit_provider_price": 100000,
"qty_ordered": 10,
"qty_confirmed": 10,
"qty_dispatched": 10,
"qty_delivered": 10,
"qty_received": 0,
"qty_rejected": 0,
"line_provider_total": 1000000
}
],
"provider_total": 1360000,
"sla_due_at": "2026-11-07T16:18:33Z",
"offer_expires_at": "2026-11-09T16:18:33Z",
"dispatches": [
{
"id": "a32964ea-3019-489d-a64c-96ff8c9182e2",
"lines": [
{
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"qty": 10
}
],
"carrier": {
"name": "Driver",
"phone": "+256700000000",
"vehicle": "UAX 123B"
},
"dispatched_at": "2026-11-06T16:19:35Z",
"eta_from": null,
"eta_to": null,
"notes": null
}
],
"deliveries": [
{
"id": "a32964ea-3556-4312-a653-47054d8ad35b",
"lines": [
{
"order_line_id": "a32964e9-ff30-4959-8590-57d6173b88e2",
"qty": 10
}
],
"delivered_at": "2026-11-06T16:19:35Z",
"delivered_to_name": "Mary"
}
],
"payment": null,
"created_at": "2026-11-06T16:18:33Z"
}
}/provider/paymentsSupplierPayments to the company. Reference ends in -L (lender part) or -S (Ensibuuko part).
Request
GET /api/inputmarket/v1/provider/payments
Response
HTTP 200
{
"data": [
{
"id": "a32964ea-48a0-498b-a9cc-c9c9c8f626ed",
"order_id": "a32964e9-fe70-4e98-9dde-56d03dd64881",
"order_reference": "IM-2026-000001",
"amount": 1360000,
"status": "PAID",
"reference": "IMP-IM-2026-000001-L",
"attempts": 1,
"paid_at": "2026-11-06T16:19:35Z",
"failure_reason": null
}
],
"meta": {
"page": 1,
"per_page": 20,
"total": 1
}
}/provider/settlementsSupplierSettlement statements.
/provider/walletSupplierThe payout wallet: balance (null when the wallet service cannot be read), currency, withdrawal limits (min, mobile_money_max, bank_min), bank_mode (MANUAL for now), destinations (the company admins' confirmed phones that mobile money can go to) and open (amount being withdrawn).
Response
{
"wallet_id": "WL-09812",
"balance": 2000000,
"currency": "UGX",
"min": 5000,
"mobile_money_max": 5000000,
"bank_min": 100000,
"bank_mode": "MANUAL",
"destinations": [ { "phone": "+256772000981", "phone_display": "+256 772 000 981", "name": "Okello James", "registered_name": "OKELLO JAMES" } ],
"open": 0
}/provider/payoutsSupplierStep 1 of a withdrawal. Company admins only. Checks the amount against the limits and the live balance, then texts a code to the admin. Mobile money goes only to one of destinations. A bank withdrawal leaves the wallet at once and Ensibuuko pays the account by hand, usually within one working day; it is returned to the wallet if the transfer cannot be made.
| Body field | Type | Meaning |
|---|---|---|
method | string | MOBILE_MONEY or BANK. |
amount | int | Whole currency units. |
phone optional | string | For MOBILE_MONEY: one of destinations[].phone. |
bank_name, account_name, account_number optional | string | For BANK. |
branch optional | string | For BANK. |
Response
HTTP 202
{
"challenge_id": "6c0f…",
"masked_phone": "+256 7•• ••• 981",
"expires_at": "2026-11-06T18:10:00Z",
"summary": "UGX 250,000 to +256 772 000 981"
}/provider/payouts/confirmSupplierStep 2: the admin's code. The money moves now. status: PROCESSING (mobile money on its way; becomes PAID or FAILED), AWAITING_BANK (Ensibuuko is paying the bank; becomes PAID with bank_reference, or RETURNED with failure_reason), or FAILED (nothing left the wallet). The admin gets a text when it finishes.
| Body field | Type | Meaning |
|---|---|---|
challenge_id | uuid | |
code | string |
Response
HTTP 201
{
"id": "a32982eb-…",
"reference": "PO-2026-000001",
"method": "MOBILE_MONEY",
"amount": 250000,
"currency": "UGX",
"destination": { "phone": "+256772000981", "name": "Okello James", "registered_name": "OKELLO JAMES" },
"status": "PROCESSING",
"failure_reason": null,
"bank_reference": null,
"created_at": "2026-11-06T18:00:00Z",
"completed_at": null
}/provider/payoutsSupplierWithdrawals, newest first, paged {data, meta}.
| Query | Type | Meaning |
|---|---|---|
page, per_page | int |
/provider/productsSupplierThe company's products with its own prices and stock.
Request
GET /api/inputmarket/v1/provider/products
Response
HTTP 200
{
"data": [
{
"id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"name": "Maize seed (Longe 5)",
"unit": "kg",
"category_id": "a32964e9-cc8a-4c24-83fe-ea7c8267876f",
"provider_unit_price": 100000,
"availability": "IN_STOCK",
"availability_mode": "UNLIMITED",
"availability_flag": "IN_STOCK",
"stock_qty": null,
"min_qty": 1,
"max_qty": 500,
"qty_step": 1,
"active": true,
"pending_price_change": null
}
]
}/provider/products/{id}SupplierAsks for a price change. Ensibuuko approves it before groups see it.
| Body field | Type | Meaning |
|---|---|---|
provider_unit_price | int | New price per unit. |
/provider/products/{id}/availabilitySupplierUpdates stock.
| Body field | Type | Meaning |
|---|---|---|
availability | string | IN_STOCK, LIMITED or OUT_OF_STOCK. |
stock_qty optional | int |
Request
PUT /api/inputmarket/v1/provider/products/a32964e9-cf34-4db6-a3d0-79e3a2eb5b87/availability
{
"availability": "LIMITED",
"stock_qty": 40
}Response
HTTP 200
{
"id": "a32964e9-cf34-4db6-a3d0-79e3a2eb5b87",
"name": "Maize seed (Longe 5)",
"unit": "kg",
"category_id": "a32964e9-cc8a-4c24-83fe-ea7c8267876f",
"provider_unit_price": 100000,
"availability": "IN_STOCK",
"availability_mode": "UNLIMITED",
"availability_flag": "IN_STOCK",
"stock_qty": 40,
"min_qty": 1,
"max_qty": 500,
"qty_step": 1,
"active": true,
"pending_price_change": null
}For companies buying harvest. Paths start /offtaker; /buyer is an older alias. The group's name and members are shown only once a commitment is binding; before that, listings show totals and the district.
/offtaker/listingsOfftakerHarvest open to this offtaker in its country.
Response
HTTP 200
{ "data": [ { "id": "…", "crop": "Maize", "qty_min_kg": 5000, "qty_max_kg": 9000, "pledged_kg": 4000,
"members_pledged": 1, "district": "Mukono", "region": "Central", "ready_from": "2026-12-06",
"quality_spec": null, "status": "PUBLISHED", "detail_level": "AGGREGATE" } ] }/offtaker/listings/{id}OfftakerOne listing. With detail_level: COMMITTED it adds the group, members and plot pins.
/offtaker/listings/{id}/offersOfftakerOffers to buy.
| Body field | Type | Meaning |
|---|---|---|
qty_max_kg | number | |
pickup_from, pickup_to optional | date | |
payment_terms_hours optional | int | Pay within this many hours of the weigh-in. |
quality_spec optional | object | |
conditions optional | string |
Request
POST /api/inputmarket/v1/offtaker/listings/a32964ea-6eb3-475f-ba5c-addc48e7cffb/offers
{
"qty_max_kg": 4000,
"payment_terms_hours": 48,
"pickup_from": "2026-12-06"
}Response
HTTP 201
{
"id": "a32964ea-77bd-4df0-ac6c-b0dcf0814fa9",
"listing_id": "a32964ea-6eb3-475f-ba5c-addc48e7cffb",
"version": 1,
"qty_max_kg": 4000,
"pickup_from": "2026-12-06",
"pickup_to": null,
"payment_terms_hours": 48,
"quality_spec": null,
"conditions": null,
"expires_at": "2026-11-09T16:20:06Z",
"status": "OPEN"
}/offtaker/offers/{id}OfftakerChanges an open offer (a new version), or withdraws it.
/offtaker/listings/{id}/price-proposalsOfftakerOffers a price per kg. The group accepts or declines; it cannot counter. One open price at a time.
| Body field | Type | Meaning |
|---|---|---|
price_per_kg | int | |
valid_until optional | datetime | Default 24 hours. |
quality_terms optional | object | |
pickup_id optional | uuid | Price for one pickup only. |
Request
POST /api/inputmarket/v1/offtaker/listings/a32964ea-6eb3-475f-ba5c-addc48e7cffb/price-proposals
{
"price_per_kg": 1200
}Response
HTTP 201
{
"id": "a32964ea-8b75-4a07-b1c4-cf925e733ba7",
"listing_id": "a32964ea-6eb3-475f-ba5c-addc48e7cffb",
"commitment_id": "a32964ea-7a8d-478f-983f-ac7c46b23737",
"mode": "AT_OFFTAKE",
"price_per_kg": 1200,
"valid_until": "2026-11-07T16:20:06Z",
"quality_terms": null,
"status": "OPEN"
}/offtaker/price-proposals/{id}OfftakerWithdraws an open price.
/offtaker/commitmentsOfftakerThe offtaker's commitments.
Request
GET /api/inputmarket/v1/offtaker/commitments
HTTP 200
{
"data": [
{
"id": "a32964ea-7a8d-478f-983f-ac7c46b23737",
"listing_id": "a32964ea-6eb3-475f-ba5c-addc48e7cffb",
"terms_version": 1,
"terms": {
"version": 1,
"buyer": {
"id": "a32964ea-6ce9-4e2a-ac8e-1b35f899c6c8",
"name": "Grain Buyer Ltd"
},
"crop": "Maize",
"qty_kg": 4000,
"tolerance_pct": 10,
"pickup_from": "2026-12-06",
"pickup_to": null,
"payment_terms_hours": 48,
"quality": null,
"price": "Set by the buyer before pickup. The group accepts or declines it with an officer code.",
"loans": "Selling the harvest does not change what the group owes on any loan.",
"blocks": [
{
"title": "What the group sells",
"body": "Maize, about 4,000 kg (±10%)."
}
]
},
"status": "FULFILLED",
"binding_at": "2026-11-06T16:20:06Z",
"signatures": [
{
"party": "GROUP_OFFICER",
"terms_version": 1,
"signed_at": "2026-11-06T16:20:06Z"
}
]
}
]
}/offtaker/commitments/{id}/signOfftakerSigns the commitment.
| Body field | Type | Meaning |
|---|---|---|
terms_version | int | The version you read. |
Request
POST /api/inputmarket/v1/offtaker/commitments/a32964ea-7a8d-478f-983f-ac7c46b23737/sign
{
"terms_version": 1
}HTTP 200
{
"id": "a32964ea-7a8d-478f-983f-ac7c46b23737",
"listing_id": "a32964ea-6eb3-475f-ba5c-addc48e7cffb",
"terms_version": 1,
"terms": {
"version": 1,
"buyer": {
"id": "a32964ea-6ce9-4e2a-ac8e-1b35f899c6c8",
"name": "Grain Buyer Ltd"
},
"crop": "Maize",
"qty_kg": 4000,
"tolerance_pct": 10,
"pickup_from": "2026-12-06",
"pickup_to": null,
"payment_terms_hours": 48,
"quality": null,
"price": "Set by the buyer before pickup. The group accepts or declines it with an officer code.",
"loans": "Selling the harvest does not change what the group owes on any loan.",
"blocks": [
{
"title": "What the group sells",
"body": "Maize, about 4,000 kg (±10%)."
}
]
},
"status": "PARTIALLY_SIGNED",
"binding_at": null,
"signatures": [
{
"party": "GROUP_OFFICER",
"terms_version": 1,
"signed_at": "2026-11-06T16:20:06Z"
}
]
}/offtaker/pickupsOfftakerBooks a pickup on a binding commitment.
| Body field | Type | Meaning |
|---|---|---|
commitment_id | uuid | |
scheduled_at | datetime | |
vehicle optional | string | |
rep optional | object | {name, phone, phone_token}. A phone needs phone_token from /offtaker/phone-checks, unless it is a teammate's phone confirmed when they joined. |
location optional | object |
Request
POST /api/inputmarket/v1/offtaker/pickups
{
"commitment_id": "a32964ea-7a8d-478f-983f-ac7c46b23737",
"scheduled_at": "2026-11-07T16:20:06+00:00",
"vehicle": "UBF 456K",
"rep": {
"name": "Peter",
"phone": "+256772000777"
}
}Response
HTTP 201
{
"id": "a32964ea-9198-4943-ae3d-4c312c36ee67",
"status": "SCHEDULED",
"scheduled_at": "2026-11-07T16:20:06Z"
}/offtaker/pickups/{id}/arrivedOfftakerMarks arrival at the group.
Request
POST /api/inputmarket/v1/offtaker/pickups/a32964ea-9198-4943-ae3d-4c312c36ee67/arrived
Response
HTTP 200
{
"id": "a32964ea-9198-4943-ae3d-4c312c36ee67",
"status": "ARRIVED"
}/offtaker/weigh-insOfftakerRecords a weigh ticket. Net = gross − tare; members' shares follow their pledges unless per_member is given.
| Body field | Type | Meaning |
|---|---|---|
pickup_id | uuid | |
ticket_no | string | |
gross_kg | number | |
tare_kg optional | number | |
moisture_pct optional | number | |
grade optional | string | |
deductions optional | array | [{code, label, amount}], shown to the group. |
per_member optional | array | [{member_id, kg}] adding up to net. |
evidence_ids optional | array | |
supersedes_id optional | uuid | Re-weigh of a disputed ticket. |
Request
POST /api/inputmarket/v1/offtaker/weigh-ins
{
"pickup_id": "a32964ea-9198-4943-ae3d-4c312c36ee67",
"ticket_no": "T-1",
"gross_kg": 4100,
"tare_kg": 100,
"moisture_pct": 12.5,
"grade": "Grade 1"
}Response
HTTP 201
{
"id": "a32964ea-94c3-43d1-b208-3c79f828d8c7",
"pickup_id": "a32964ea-9198-4943-ae3d-4c312c36ee67",
"ticket_no": "T-1",
"gross_kg": 4100,
"tare_kg": 100,
"net_kg": 4000,
"moisture_pct": 12.5,
"grade": "Grade 1",
"deductions": [],
"per_member": [
{
"member_id": "104",
"kg": 4000
}
],
"confirmations": [],
"status": "DRAFT",
"supersedes_id": null,
"settlement_id": null
}/offtaker/weigh-ins/{id}/confirmOfftakerThe offtaker confirms its own ticket. The group confirms too.
Request
POST /api/inputmarket/v1/offtaker/weigh-ins/a32964ea-94c3-43d1-b208-3c79f828d8c7/confirm
HTTP 200
{
"id": "a32964ea-94c3-43d1-b208-3c79f828d8c7",
"pickup_id": "a32964ea-9198-4943-ae3d-4c312c36ee67",
"ticket_no": "T-1",
"gross_kg": 4100,
"tare_kg": 100,
"net_kg": 4000,
"moisture_pct": 12.5,
"grade": "Grade 1",
"deductions": [],
"per_member": [
{
"member_id": "104",
"kg": 4000
}
],
"confirmations": [
{
"party": "BUYER_REP",
"user_id": "a32964ea-6d0f-4efb-9d8a-d67fb46d93f2",
"at": "2026-11-06T16:20:06Z",
"otp": false
}
],
"status": "DRAFT",
"supersedes_id": null,
"settlement_id": null
}/offtaker/settlementsOfftakerWhat the offtaker owes each group.
Request
GET /api/inputmarket/v1/offtaker/settlements
Response
HTTP 200
{
"data": [
{
"id": "a32964ea-9a51-4d55-ad34-add04838b0b5",
"weigh_in_id": "a32964ea-94c3-43d1-b208-3c79f828d8c7",
"price_per_kg": 1200,
"gross_value": 4800000,
"other_deductions": [],
"net_to_group": 4800000,
"status": "COMPUTED",
"balance_due_at": "2026-11-08T16:20:06Z",
"paid_to_group_at": null,
"payment_reference": null
}
]
}/offtaker/settlements/{id}/pay-groupOfftakerRecords that the group was paid. The group then confirms receipt.
| Body field | Type | Meaning |
|---|---|---|
reference | string | Mobile-money or bank reference. |
Request
POST /api/inputmarket/v1/offtaker/settlements/a32964ea-9a51-4d55-ad34-add04838b0b5/pay-group
{
"reference": "MM-123"
}Response
HTTP 200
{
"id": "a32964ea-9a51-4d55-ad34-add04838b0b5",
"weigh_in_id": "a32964ea-94c3-43d1-b208-3c79f828d8c7",
"price_per_kg": 1200,
"gross_value": 4800000,
"other_deductions": [],
"net_to_group": 4800000,
"status": "BALANCE_PAID",
"balance_due_at": "2026-11-08T16:20:06Z",
"paid_to_group_at": "2026-11-06T16:20:06Z",
"payment_reference": "MM-123"
}/offtaker/feesOfftakerFees the offtaker owes Ensibuuko per kg bought. Never shown to or taken from groups.
/offtaker/evidenceOfftakerUploads a weigh-in photo (multipart), slots scale, loaded_truck, moisture_meter.
For the wallet service only. It reports the result of each transfer the Marketplace started. Unknown references return 404.
/webhooks/paymentsSignedHeader X-Inputmarket-Signature: sha256=<hex HMAC-SHA256 of the raw body> using the shared secret. A bad or missing signature returns 401.
| Body field | Type | Meaning |
|---|---|---|
reference | string | The transfer reference the Marketplace sent (for example IMP-IM-2026-000001-L). |
status | string | SUCCESS, FAILED or PENDING. |
failure_reason optional | string |
Request
POST /api/inputmarket/v1/webhooks/payments
Content-Type: application/json
X-Inputmarket-Signature: sha256=5d1f…
{ "reference": "IMP-IM-2026-000001-L", "status": "SUCCESS" }Response
HTTP 200
{ "ok": true, "kind": "provider_payment" } // or "repayment"Every endpoint in this document, in order. Paths are relative to https://market.stg.chomoka.org/api/inputmarket/v1.