← Chomoka Marketplace Chomoka Marketplace · API v1

Chomoka Marketplace API

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.

Download this guide Download OpenAPI spec The OpenAPI file imports into Postman, Insomnia or a code generator. Live copy: https://market.stg.chomoka.org/developers/openapi.json

Overview

Base URLhttps://market.stg.chomoka.org/api/inputmarket/v1

The 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.

Group appThe Chomoka Android app, signed in as a savings group. Orders, signing, delivery, loans, farm records, harvest sales.
AgentsField agents helping a group, on the group's phone, with a short-lived Agent session.
SuppliersCompanies selling inputs. Answer, prepare, dispatch and deliver orders; see payments; keep products current.
OfftakersCompanies buying harvest. Offer, sign, book pickups, weigh, pay groups.

How an order flows

  1. The group quotes a cart, splits it between members, and each member consents with a code to their phone.
  2. The group sends the order. One order is created per supplier.
  3. The supplier confirms (all, part, or none). Some lenders approve each loan at this point.
  4. Officers review the loan offer and sign with codes to their own phones.
  5. The supplier prepares, dispatches and delivers, with photos.
  6. The group confirms what it received. The lender pays the supplier, Ensibuuko pays any part paid from the group wallet, and the group's loan starts for what was received.

Money, time and IDs

  • Amounts are whole numbers in the group's currency (config.currency; UGX has no decimals).
  • Times are ISO 8601 in UTC with a Z; dates are YYYY-MM-DD.
  • Marketplace ids are UUID strings. Group and member ids are the strings the Chomoka app already uses.
  • The group app only ever sees what groups pay. Supplier prices and Ensibuuko's fee are never sent to it.

Sign-in

CallerSend on every request
Group appAuthorization: Basic base64(group_key:group_secret), the same credentials the rest of the app uses, and X-Device-Id.
Agent acting for a groupThe 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 systemAuthorization: Bearer <token> from phone sign-in. The phone must be registered by Ensibuuko for the company. Tokens last 30 days.
Wallet serviceX-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.

Requests and responses

HeaderUse
Idempotency-KeyA 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-IdA stable id for the phone. Used for rate limits and to bind Agent sessions.
X-Client-Created-AtWhen 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-Languageen, sw, fr, lg or rw. Texts come back in that language (Content-Language).
If-None-MatchGroup 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.

Errors

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"
    }
  }
}
CodeHTTPMeaning and what to do
UNAUTHENTICATED401Missing or wrong credentials, or an expired token. Sign in again.
FORBIDDEN403Signed in but not allowed. details.reason may be AGENT_SESSION_EXPIRED, AGENT_NOT_ASSIGNED, AGENT_WRONG_DEVICE, AGENT_REQUIRED.
NOT_ELIGIBLE403The Marketplace is not open to this group; details.reason says why.
NOT_FOUND404No such item for this caller.
NOT_SUPPORTED404The action exists in the design but is switched off (for example countering).
VALIDATION_ERROR422Bad or missing input. details names the field or lists missing.
INVALID_STATE409The item is not in a state that allows this (for example signing an expired offer).
STALE_STATE409Something changed since the app last loaded it. Reload and retry.
DUPLICATE_REQUEST409This Idempotency-Key was already used for a different request.
CREDIT_LIMIT_EXCEEDED422The order needs more credit than the group has.
PRODUCT_UNAVAILABLE422Out of stock, hidden, or not sold to this group.
PRICE_CHANGED409A price changed since the quote. Quote again and show the difference.
OFFER_EXPIRED409The offer can no longer be signed.
ALREADY_SIGNED409This officer already signed.
OTP_INVALID / OTP_EXPIRED / OTP_LOCKED422Wrong code, code too old (10 minutes), or too many wrong codes (3). Ask for a new code.
RATE_LIMITED429Too many code requests. Wait before asking again.
WALLET_INSUFFICIENT422The group wallet cannot cover the wallet part.
PAYMENT_METHOD_NOT_ALLOWED422That payment method is switched off for this group.
EVIDENCE_REQUIRED / EVIDENCE_EXPIRED409 / 422Required photos are missing, or a photo is too old for this step.
HASH_MISMATCH / INTEGRITY_FAILED422An upload's checksum or content did not match.
ALLOCATION_MISMATCH422Member shares do not add up to the order lines.
MEMBER_INELIGIBLE422A member cannot receive inputs (inactive, or no plot when one is required).
CONSENT_REQUIRED / CONSENT_CAP_EXCEEDED422A member's share has no valid consent, or is above the cap for that consent method.
AGENT_ACTION_REQUIRES_LIVE422An Agent step was sent from the offline queue; do it again live.
FUNDING_UNAVAILABLE / FUNDING_PENDING422 / 409No lender can fund this loan, or the lender has not approved yet.
SERVER_ERROR500Our fault. Retry with the same Idempotency-Key.

Statuses

FieldValues
Order app_statusWAITING_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.fulfilmentSUBMITTED, PROVIDER_REVIEW, OFFER_READY, OFFER_ACCEPTED, PREPARING, DISPATCHED, AWAITING_GROUP_CONFIRMATION, DELIVERED_CONFIRMED, CLOSED, DISPUTED, REJECTED, EXPIRED, DECLINED_BY_GROUP, CANCELLED
status.paymentNOT_DUE, PENDING, PROCESSING, PAID, FAILED, HELD_FOR_REVIEW
status.loanNONE, RESERVED, ACTIVE, IN_ARREARS, REPAID
CommitmentDRAFT, PARTIALLY_SIGNED, BINDING, FULFILLED, CANCELLED, BREACHED
PickupSCHEDULED, ARRIVED, WEIGHED, CANCELLED, NO_SHOW
Weigh ticketDRAFT, CONFIRMED, DISPUTED, REWEIGHED
SettlementCOMPUTED (offtaker to pay), BALANCE_PAID, GROUP_CONFIRMED
Price proposal / offerOPEN, ACCEPTED, DECLINED, EXPIRED, WITHDRAWN

Start-up and context

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.

GET/pingNo auth

Health 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"
}
GET/me/contextGroup

Everything 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
Response HTTP 200 · 88 lines
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
    }
  }
}
GET/configGroup

All 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": "…"
}
GET/creditGroup

The 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
}
POST/consentGroup

Records that the group accepted the Marketplace terms. Send the current_version from /me/context.

Body fieldTypeMeaning
versionstringThe consent text version shown to the group.
POST/devicesGroup

Registers the phone (push token, app version). Optional.

GET/notificationsGroup

Messages for the group, newest first, paged.

QueryTypeMeaning
pageintPage, from 1.
per_pageintItems per page (default 20).

Request

GET /api/inputmarket/v1/notifications

Response

HTTP 200
{
  "data": [],
  "meta": {
    "page": 1,
    "per_page": 20,
    "total": 0
  }
}
POST/notifications/{id}/readGroup

Marks one notification read.

Phone checks

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.

POST/phone-checksGroup

Sends a code to the phone. purpose is one of DRIVER, PICKUP_REP, DELIVERY_RECEIVER, TEAM_MEMBER, COMPANY_CONTACT, JOIN.

Body fieldTypeMeaning
purposestringWhat the phone is for.
phonestringInternational, or local for country.
country optionalstringISO code for a local number.
namestringThe 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"
}
POST/phone-checks/{id}/confirmGroup

Checks 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 fieldTypeMeaning
codestringThe 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. …"
}
POST/phone-checks/{id}/use-registered-nameGroup

After 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.

POST/provider/phone-checksSupplier

Same as POST /phone-checks, for supplier integrations (drivers, team members, company contact).

POST/provider/phone-checks/{id}/confirmSupplier

Same as POST /phone-checks/{id}/confirm.

POST/offtaker/phone-checksOfftaker

Same as POST /phone-checks, for offtaker integrations (pickup reps).

POST/offtaker/phone-checks/{id}/confirmOfftaker

Same as POST /phone-checks/{id}/confirm.

Catalogue

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}.

GET/categoriesGroup

Product 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
  }
]
GET/productsGroup

Products the group can order, from suppliers serving its country and district.

QueryTypeMeaning
category_iduuidOnly this category.
qstringSearch by name.
sortstringSort order.
page, per_pageintAsk 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
  }
]
GET/products/{id}Group

One 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."
}
GET/providersGroup

Suppliers 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
  }
]
GET/providers/{id}Group

One supplier.

Cart, members and consent

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.

POST/cart/quoteGroup

Prices 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 fieldTypeMeaning
linesarray[{product_id, qty}]
payment_plan optionalobject{method: LOAN | WALLET | SPLIT, wallet_amount?}
allocations optionalarrayMember 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"
  }
}
Response HTTP 200 · 57 lines
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": []
}
GET/membersGroup

Members 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
    }
  ]
}
POST/orders/allocations/suggestGroup

Proposes how to split each line between members. subject_hash is what that member's consent must cover.

Body fieldTypeMeaning
linesarray[{product_id, qty}]
member_idsarrayMembers 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"
    }
  ]
}
POST/consents/challengeGroup

Sends a code to the member's phone for their share. Use purpose: PLEDGE with subject_type: LISTING and qty_kg for harvest pledges.

Body fieldTypeMeaning
member_idstringMember id.
purpose optionalstringALLOCATION (default) or PLEDGE.
subject_type optionalstringORDER_DRAFT or LISTING.
subject_idstringThe draft's Idempotency-Key, or the listing id.
allocation optionalarray[{product_id, qty}] for ALLOCATION.
qty_kg optionalnumberFor 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
}
POST/consents/verifyGroup

Checks the member's code and records the consent.

Body fieldTypeMeaning
challenge_iduuid
otpstringThe 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"
}
POST/consents/witnessedGroup + Agent

Consent 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 fieldTypeMeaning
member_idstring
subject_idstringDraft key.
allocationarray
signature_evidence_iduuidUploaded signature.
photo_evidence_iduuidUploaded member photo.
witness_user_ids optionalarray
POST/consents/{id}/withdrawGroup

Withdraws a consent before the order is sent.

POST/members/{id}/phonesGroup

Adds a phone for a member (own or shared) and sends it a code.

Body fieldTypeMeaning
msisdnstringPhone number.
kindstringOWN or SHARED.
holder_name optionalstringWho holds a shared phone.
relationship optionalstring
POST/members/{id}/phones/verifyGroup

Confirms 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 fieldTypeMeaning
challenge_iduuid
otpstring

Orders

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.

POST/ordersGroup

Sends the order. Idempotency-Key is required and must equal the draft key used for the consents. Every member share needs a consent.

Body fieldTypeMeaning
linesarray[{product_id, qty}]
allocationsarray[{member_id, lines:[{product_id, qty}]}]
deliveryobject{use_group_location, location_id?, landmark?, preferred_window?}
payment_planobject{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"
  }
}
Response HTTP 201 · 85 lines
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
}
GET/ordersGroup

The group's orders, paged.

QueryTypeMeaning
bucketstringactive, needs_action or done.
page, per_pageint

Request

GET /api/inputmarket/v1/orders?bucket=active
Response HTTP 200 · 31 lines
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
  }
}
GET/orders/{id}Group

One order with lines, delivery, timeline, dispatches and, once delivered, its loan.

Request

GET /api/inputmarket/v1/orders/a32964e9-fe70-4e98-9dde-56d03dd64881
Response HTTP 200 · 123 lines
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
}
GET/orders/{id}/allocationsGroup

Who 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
    }
  ]
}
POST/orders/{id}/cancelGroup

Cancels before the supplier starts preparing.

Body fieldTypeMeaning
reason optionalstring

Loan offer and signing

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.

GET/orders/{id}/offerGroup

The 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
Response HTTP 200 · 119 lines
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."
}
POST/orders/{id}/offer/otpGroup

Sends a code to one officer.

Body fieldTypeMeaning
signatory_idstringOfficer'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"
}
POST/orders/{id}/offer/signGroup

One officer signs. When enough have signed, status becomes ACCEPTED and the supplier prepares the order.

Body fieldTypeMeaning
signatory_idstring
challenge_iduuid
otpstring
accepted_termsboolMust 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
}
Response HTTP 200 · 127 lines
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"
}
POST/orders/{id}/offer/declineGroup

The group declines the offer. The order closes and the reserved credit is released.

Body fieldTypeMeaning
reason optionalstring

Delivery and photos

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.

GET/orders/{id}/dispatchesGroup

What 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
    }
  ]
}
GET/orders/{id}/delivery-confirmation/templateGroup

The 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
Response HTTP 200 · 73 lines
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."
}
GET/evidence/policyGroup

Photo rules for a checkpoint and what is still missing.

QueryTypeMeaning
checkpointstringRECEIPT, LOADED, DROPOFF, CHECKIN_*, WEIGHIN.
order_iduuidThe 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
    }
  ]
}
POST/evidenceGroup

Uploads one photo as multipart/form-data. Idempotency-Key is required. For large files or poor networks use the resumable upload below.

Body fieldTypeMeaning
filefileJPEG photo.
subject_typestringORDER_RECEIPT for the group.
subject_iduuidOrder id.
slot_codestringline_photo, damage_closeup, signature, group_with_goods.
order_line_id optionaluuidFor per-line photos.
captured_atdatetimeWhen the photo was taken.
gps optionalobject{lat, lng}
captured_offline optionalbool
supersedes_id optionaluuidReplaces 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
}
POST/uploadsGroup

Starts 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 }
PATCH/uploads/{id}Group

Sends 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.

GET/uploads/{id}Group

Where an upload stands after a lost connection: headers Upload-Offset, Upload-Length, Upload-Status.

POST/uploads/{id}/completeGroup

Finishes the upload and returns the same evidence object as POST /evidence.

POST/orders/{id}/delivery-confirmation/otpGroup

Sends a code to the officer confirming.

Body fieldTypeMeaning
officer_idstring

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"
}
POST/orders/{id}/delivery-confirmationsGroup

Confirms delivery. Idempotency-Key is required. Quantities refused need a reason_code and a damage photo. Refusing everything opens a dispute instead.

Body fieldTypeMeaning
linesarray[{order_line_id, qty_received, qty_rejected, reason_code?}]
photo_idsarrayEvidence ids.
signature_iduuid
gpsobject{lat, lng}
receiver optionalobject{name, phone, phone_token}. phone_token from phone checks, unless the phone is the confirming officer's.
challenge_id, otpstringThe officer's code.
exception_reason_code optionalstring
notes optionalstring

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."
}
POST/orders/{id}/disputesGroup

Reports a problem with an order. Ensibuuko reviews it; payment to the supplier waits.

Body fieldTypeMeaning
reason_codestringOne of config.dispute_reasons.
notes optionalstring
evidence_ids optionalarray
GET/orders/{id}/disputesGroup

Disputes on an order.

GET/orders/{id}/evidenceGroup

Photos recorded on an order.

Loans and repayments

A loan starts when the supplier is paid. Groups see principal, schedule and what they owe; never commission or supplier amounts.

GET/loansGroup

The group's Marketplace loans, paged.

Request

GET /api/inputmarket/v1/loans
Response HTTP 200 · 36 lines
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
  }
}
GET/loans/{id}Group

One loan with its schedule.

Request

GET /api/inputmarket/v1/loans/a32964ea-4fe5-4e07-b333-4756b3abda9f
Response HTTP 200 · 27 lines
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
  }
}
GET/loans/{id}/repaymentsGroup

Repayments received: [{id, amount, channel, reference, received_at}].

Request

GET /api/inputmarket/v1/loans/a32964ea-4fe5-4e07-b333-4756b3abda9f/repayments

Response

HTTP 200
{
  "data": []
}
GET/repayments/nextGroup

The 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"
    }
  ]
}
POST/loans/{id}/repaymentsGroup

Direct mobile-money repayment. Off until features.direct_repayment is true; groups repay through the normal Chomoka repayment flow.

Body fieldTypeMeaning
amountint
msisdnstring

Offline sync

The app works offline and sends queued actions later. Send X-Client-Created-At with the time the action was taken on the phone.

GET/sync/snapshotGroup

Everything 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.

QueryTypeMeaning
sincedatetimeLast server_time you saved. Omit for a full snapshot.

Request

GET /api/inputmarket/v1/sync/snapshot
Response HTTP 200 · 84 lines
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"
    }
  ]
}

Agent sessions

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.

POST/agent/challengeGroup

Starts sign-in. identifier is the Agent code or the Agent's phone.

Body fieldTypeMeaning
identifierstringAgent 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"
}
POST/agent/verifyGroup

Returns the authority_token and what the Agent may do (capabilities).

Body fieldTypeMeaning
challenge_iduuid
otpstring

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"
  ]
}
DELETE/agent/sessionGroup + Agent

Ends 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 204

Farm records

Members' plots and season plans, used for loans, check-ins and harvest sales.

GET/seasonsGroup

Seasons.

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"
    }
  ]
}
GET/farm/plotsGroup

Plots, optionally for one member.

QueryTypeMeaning
member_idstringOnly 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"
    }
  ]
}
POST/farm/plotsGroup

Adds a plot.

Body fieldTypeMeaning
member_idstring
namestring
area_declared_acres optionalnumber
geo_point optionalobject{lat, lng}
tenure optionalstring
answers optionalobjectForm 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"
}
PUT/farm/plots/{id}Group

Updates a plot.

POST/farm/plots/{id}/boundaryGroup

Saves a walked boundary as a GeoJSON Polygon. Area is measured and compared with the declared area.

Body fieldTypeMeaning
boundaryGeoJSONPolygon.
gps_accuracy_m optionalnumber
GET/farm/summaryGroup

How 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"
  }
}
GET/farm/plots/{id}/seasons/{seasonId}Group

A plot's plan for a season (crop, variety, planting date, expected yield).

PUT/farm/plots/{id}/seasons/{seasonId}Group

Saves that plan.

GET/farm/checkins/dueGroup

Field check-ins due.

POST/farm/checkinsGroup

Submits a check-in with photos.

Body fieldTypeMeaning
plot_season_iduuid
stagestringPLANTING, GERMINATION, MIDSEASON, PREHARVEST.
evidence_ids optionalarray
agent_present optionalbool
GET/forms/{key}Group

A dynamic form definition (questions for plots and seasons).

Harvest sales (group side)

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.

GET/offtake/forecastGroup

Expected harvest from the farm records.

GET/offtake/listingsGroup

The group's listings.

POST/offtake/listingsGroup

Creates a draft listing.

Body fieldTypeMeaning
cropstring
qty_min_kg, qty_max_kgnumber
season_id optionaluuid
ready_from, ready_to optionaldate
quality_spec optionalobject
pickup_location optionalobject

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"
}
PUT/offtake/listings/{id}Group

Updates a draft listing.

POST/offtake/listings/{id}/publishGroup

Shows 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"
}
POST/offtake/listings/{id}/pledgesGroup

A member pledges kilograms, with a PLEDGE consent.

Body fieldTypeMeaning
member_idstring
qty_kgnumber
consent_iduuidFrom /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"
    }
  ]
}
GET/offtake/listings/{id}/offersGroup

Offers 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"
    }
  ]
}
POST/offtake/offers/{id}/acceptGroup

Accepts 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
Response HTTP 201 · 34 lines
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": []
}
POST/offtake/offers/{id}/declineGroup

Declines an offer.

GET/offtake/commitments/{id}Group

A commitment with its terms and signatures.

Request

GET /api/inputmarket/v1/offtake/commitments/a32964ea-7a8d-478f-983f-ac7c46b23737
Response HTTP 200 · 42 lines
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
    }
  ]
}
POST/offtake/commitments/{id}/otpGroup

Sends a code to an officer.

Body fieldTypeMeaning
officer_idstring

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"
}
POST/offtake/commitments/{id}/signGroupGroup + Agent

An 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 fieldTypeMeaning
challenge_id, otpstringOfficer's code (not needed for the Agent witness).
terms_versionintThe 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
}
Response HTTP 200 · 42 lines
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
    }
  ]
}
GET/offtake/price-proposalsGroup

Prices 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
    }
  ]
}
POST/offtake/price-proposals/{id}/otpGroup

Code for the officer accepting a price.

Body fieldTypeMeaning
officer_idstring
POST/offtake/price-proposals/{id}/acceptGroup

Accepts the price.

Body fieldTypeMeaning
challenge_id, otpstring

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"
}
POST/offtake/price-proposals/{id}/declineGroup

Declines the price.

GET/offtake/pickupsGroup

Pickups 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"
    }
  ]
}
POST/offtake/pickups/{id}/readyGroup

The group says the harvest is ready.

Body fieldTypeMeaning
actual_qty_kg optionalnumber
moisture_pct optionalnumber
evidence_ids optionalarray
GET/offtake/weigh-ins/{id}Group

A weigh ticket.

POST/offtake/weigh-ins/{id}/otpGroup

Code for the officer confirming the weight.

Body fieldTypeMeaning
officer_idstring
POST/offtake/weigh-ins/{id}/confirmGroup

Confirms the weight. With a price accepted, the settlement is calculated (settlement_id).

Body fieldTypeMeaning
challenge_id, otpstring

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"
}
Response HTTP 200 · 29 lines
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"
}
POST/offtake/weigh-ins/{id}/disputeGroup

Disputes the weight; the offtaker must re-weigh.

Body fieldTypeMeaning
notes optionalstring
GET/offtake/settlements/{id}Group

What 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
}
POST/offtake/settlements/{id}/confirm-receivedGroup

The 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"
}

Supplier API

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.

POST/provider/auth/otpNo auth

Sends a sign-in code to a phone Ensibuuko registered for the company.

Body fieldTypeMeaning
phonestringInternational (+256…) or local form.
provider_id optionaluuidWhen 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"
}
POST/provider/auth/verifyNo auth

Returns a bearer token valid for 30 days. Send it as Authorization: Bearer <token>.

Body fieldTypeMeaning
challenge_iduuid
otpstring

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"
  }
}
POST/provider/auth/logoutSupplier

Revokes the token.

GET/provider/ordersSupplier

Orders for the company, paged, with the group's delivery place and contacts.

Request

GET /api/inputmarket/v1/provider/orders
Response HTTP 200 · 75 lines
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
  }
}
GET/provider/orders/{id}Supplier

One order. Opening a new order marks it as being reviewed.

Request

GET /api/inputmarket/v1/provider/orders/a32964e9-fe70-4e98-9dde-56d03dd64881
Response HTTP 200 · 66 lines
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"
}
POST/provider/orders/{id}/respondSupplier

Answers a new order before sla_due_at.

Body fieldTypeMeaning
decisionstringACCEPT, PARTIAL or REJECT.
lines optionalarrayFor PARTIAL: [{order_line_id, qty_confirmed}].
reason optionalstringRequired for REJECT.

Request

POST /api/inputmarket/v1/provider/orders/a32964e9-fe70-4e98-9dde-56d03dd64881/respond

{
  "decision": "ACCEPT",
  "lines": []
}
Response HTTP 200 · 66 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"
}
POST/provider/orders/{id}/statusSupplier

Moves an order to PREPARING, or CANCELLED with a reason.

Body fieldTypeMeaning
statusstringPREPARING or CANCELLED.
reason optionalstring

Request

POST /api/inputmarket/v1/provider/orders/a32964e9-fe70-4e98-9dde-56d03dd64881/status

{
  "status": "PREPARING"
}
Response HTTP 200 · 66 lines
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"
}
POST/provider/evidenceSupplier

Uploads 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
}
POST/provider/orders/{id}/dispatchesSupplier

Records goods leaving. Needs the loading photos first. Without lines, everything confirmed is dispatched.

Body fieldTypeMeaning
carrierobject{name, phone, phone_token, vehicle}. A phone needs phone_token from /provider/phone-checks; the saved name is the confirmed one.
lines optionalarray[{order_line_id, qty}]
eta_from, eta_to optionaldatetime
notes optionalstring

Request

POST /api/inputmarket/v1/provider/orders/a32964e9-fe70-4e98-9dde-56d03dd64881/dispatches

{
  "carrier": {
    "name": "Driver",
    "phone": "+256700000000",
    "vehicle": "UAX 123B"
  }
}
Response HTTP 201 · 103 lines
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"
  }
}
POST/provider/orders/{id}/deliveriesSupplier

Records goods handed over. Needs the drop-off photo. The group then confirms in the app.

Body fieldTypeMeaning
delivered_to_name optionalstring
lines optionalarray
delivered_at optionaldatetime
gps optionalobject

Request

POST /api/inputmarket/v1/provider/orders/a32964e9-fe70-4e98-9dde-56d03dd64881/deliveries

{
  "delivered_to_name": "Mary"
}
Response HTTP 201 · 100 lines
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"
  }
}
GET/provider/paymentsSupplier

Payments 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
  }
}
GET/provider/settlementsSupplier

Settlement statements.

GET/provider/walletSupplier

The 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
}
POST/provider/payoutsSupplier

Step 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 fieldTypeMeaning
methodstringMOBILE_MONEY or BANK.
amountintWhole currency units.
phone optionalstringFor MOBILE_MONEY: one of destinations[].phone.
bank_name, account_name, account_number optionalstringFor BANK.
branch optionalstringFor 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"
}
POST/provider/payouts/confirmSupplier

Step 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 fieldTypeMeaning
challenge_iduuid
codestring

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
}
GET/provider/payoutsSupplier

Withdrawals, newest first, paged {data, meta}.

QueryTypeMeaning
page, per_pageint
GET/provider/productsSupplier

The 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
    }
  ]
}
PUT/provider/products/{id}Supplier

Asks for a price change. Ensibuuko approves it before groups see it.

Body fieldTypeMeaning
provider_unit_priceintNew price per unit.
PUT/provider/products/{id}/availabilitySupplier

Updates stock.

Body fieldTypeMeaning
availabilitystringIN_STOCK, LIMITED or OUT_OF_STOCK.
stock_qty optionalint

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
}

Offtaker API

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.

GET/offtaker/listingsOfftaker

Harvest 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" } ] }
GET/offtaker/listings/{id}Offtaker

One listing. With detail_level: COMMITTED it adds the group, members and plot pins.

POST/offtaker/listings/{id}/offersOfftaker

Offers to buy.

Body fieldTypeMeaning
qty_max_kgnumber
pickup_from, pickup_to optionaldate
payment_terms_hours optionalintPay within this many hours of the weigh-in.
quality_spec optionalobject
conditions optionalstring

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"
}
PUT/offtaker/offers/{id}Offtaker

Changes an open offer (a new version), or withdraws it.

POST/offtaker/listings/{id}/price-proposalsOfftaker

Offers a price per kg. The group accepts or declines; it cannot counter. One open price at a time.

Body fieldTypeMeaning
price_per_kgint
valid_until optionaldatetimeDefault 24 hours.
quality_terms optionalobject
pickup_id optionaluuidPrice 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"
}
PUT/offtaker/price-proposals/{id}Offtaker

Withdraws an open price.

GET/offtaker/commitmentsOfftaker

The offtaker's commitments.

Request

GET /api/inputmarket/v1/offtaker/commitments
Response HTTP 200 · 40 lines
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"
        }
      ]
    }
  ]
}
POST/offtaker/commitments/{id}/signOfftaker

Signs the commitment.

Body fieldTypeMeaning
terms_versionintThe version you read.

Request

POST /api/inputmarket/v1/offtaker/commitments/a32964ea-7a8d-478f-983f-ac7c46b23737/sign

{
  "terms_version": 1
}
Response HTTP 200 · 36 lines
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"
    }
  ]
}
POST/offtaker/pickupsOfftaker

Books a pickup on a binding commitment.

Body fieldTypeMeaning
commitment_iduuid
scheduled_atdatetime
vehicle optionalstring
rep optionalobject{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 optionalobject

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"
}
POST/offtaker/pickups/{id}/arrivedOfftaker

Marks 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"
}
POST/offtaker/weigh-insOfftaker

Records a weigh ticket. Net = gross − tare; members' shares follow their pledges unless per_member is given.

Body fieldTypeMeaning
pickup_iduuid
ticket_nostring
gross_kgnumber
tare_kg optionalnumber
moisture_pct optionalnumber
grade optionalstring
deductions optionalarray[{code, label, amount}], shown to the group.
per_member optionalarray[{member_id, kg}] adding up to net.
evidence_ids optionalarray
supersedes_id optionaluuidRe-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
}
POST/offtaker/weigh-ins/{id}/confirmOfftaker

The offtaker confirms its own ticket. The group confirms too.

Request

POST /api/inputmarket/v1/offtaker/weigh-ins/a32964ea-94c3-43d1-b208-3c79f828d8c7/confirm
Response HTTP 200 · 28 lines
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
}
GET/offtaker/settlementsOfftaker

What 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
    }
  ]
}
POST/offtaker/settlements/{id}/pay-groupOfftaker

Records that the group was paid. The group then confirms receipt.

Body fieldTypeMeaning
referencestringMobile-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"
}
GET/offtaker/feesOfftaker

Fees the offtaker owes Ensibuuko per kg bought. Never shown to or taken from groups.

POST/offtaker/evidenceOfftaker

Uploads a weigh-in photo (multipart), slots scale, loaded_truck, moisture_meter.

Payment results webhook

For the wallet service only. It reports the result of each transfer the Marketplace started. Unknown references return 404.

POST/webhooks/paymentsSigned

Header X-Inputmarket-Signature: sha256=<hex HMAC-SHA256 of the raw body> using the shared secret. A bad or missing signature returns 401.

Body fieldTypeMeaning
referencestringThe transfer reference the Marketplace sent (for example IMP-IM-2026-000001-L).
statusstringSUCCESS, FAILED or PENDING.
failure_reason optionalstring

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"

All endpoints

Every endpoint in this document, in order. Paths are relative to https://market.stg.chomoka.org/api/inputmarket/v1.

GET/ping
GET/me/context
GET/config
GET/credit
POST/consent
POST/devices
GET/notifications
POST/notifications/{id}/read
POST/phone-checks
POST/phone-checks/{id}/confirm
POST/phone-checks/{id}/use-registered-name
POST/provider/phone-checks
POST/provider/phone-checks/{id}/confirm
POST/offtaker/phone-checks
POST/offtaker/phone-checks/{id}/confirm
GET/categories
GET/products
GET/products/{id}
GET/providers
GET/providers/{id}
POST/cart/quote
GET/members
POST/orders/allocations/suggest
GET/members/{id}/consent-options
POST/consents/challenge
POST/consents/verify
POST/consents/witnessed
POST/consents/{id}/withdraw
POST/members/{id}/phones
POST/members/{id}/phones/verify
POST/orders
GET/orders
GET/orders/{id}
GET/orders/{id}/allocations
POST/orders/{id}/cancel
GET/orders/{id}/offer
POST/orders/{id}/offer/otp
POST/orders/{id}/offer/sign
POST/orders/{id}/offer/decline
GET/orders/{id}/dispatches
GET/orders/{id}/delivery-confirmation/template
GET/evidence/policy
POST/evidence
POST/uploads
PATCH/uploads/{id}
GET/uploads/{id}
POST/uploads/{id}/complete
POST/orders/{id}/delivery-confirmation/otp
POST/orders/{id}/delivery-confirmations
POST/orders/{id}/disputes
GET/orders/{id}/disputes
GET/orders/{id}/evidence
GET/loans
GET/loans/{id}
GET/loans/{id}/repayments
GET/repayments/next
POST/loans/{id}/repayments
GET/sync/snapshot
POST/agent/challenge
POST/agent/verify
DELETE/agent/session
GET/seasons
GET/farm/plots
POST/farm/plots
PUT/farm/plots/{id}
POST/farm/plots/{id}/boundary
GET/farm/summary
GET/farm/plots/{id}/seasons/{seasonId}
PUT/farm/plots/{id}/seasons/{seasonId}
GET/farm/checkins/due
POST/farm/checkins
GET/forms/{key}
GET/offtake/forecast
GET/offtake/listings
POST/offtake/listings
PUT/offtake/listings/{id}
POST/offtake/listings/{id}/publish
POST/offtake/listings/{id}/pledges
GET/offtake/listings/{id}/offers
POST/offtake/offers/{id}/accept
POST/offtake/offers/{id}/decline
GET/offtake/commitments/{id}
POST/offtake/commitments/{id}/otp
POST/offtake/commitments/{id}/sign
GET/offtake/price-proposals
POST/offtake/price-proposals/{id}/otp
POST/offtake/price-proposals/{id}/accept
POST/offtake/price-proposals/{id}/decline
GET/offtake/pickups
POST/offtake/pickups/{id}/ready
GET/offtake/weigh-ins/{id}
POST/offtake/weigh-ins/{id}/otp
POST/offtake/weigh-ins/{id}/confirm
POST/offtake/weigh-ins/{id}/dispute
GET/offtake/settlements/{id}
POST/offtake/settlements/{id}/confirm-received
POST/provider/auth/otp
POST/provider/auth/verify
POST/provider/auth/logout
GET/provider/orders
GET/provider/orders/{id}
POST/provider/orders/{id}/respond
POST/provider/orders/{id}/status
POST/provider/evidence
POST/provider/orders/{id}/dispatches
POST/provider/orders/{id}/deliveries
GET/provider/payments
GET/provider/settlements
GET/provider/wallet
POST/provider/payouts
POST/provider/payouts/confirm
GET/provider/payouts
GET/provider/products
PUT/provider/products/{id}
PUT/provider/products/{id}/availability
GET/offtaker/listings
GET/offtaker/listings/{id}
POST/offtaker/listings/{id}/offers
PUT/offtaker/offers/{id}
POST/offtaker/listings/{id}/price-proposals
PUT/offtaker/price-proposals/{id}
GET/offtaker/commitments
POST/offtaker/commitments/{id}/sign
POST/offtaker/pickups
POST/offtaker/pickups/{id}/arrived
POST/offtaker/weigh-ins
POST/offtaker/weigh-ins/{id}/confirm
GET/offtaker/settlements
POST/offtaker/settlements/{id}/pay-group
GET/offtaker/fees
POST/offtaker/evidence
POST/webhooks/payments