Skip to content

API — Leads, Offers & Bids ​

The core marketplace. Sell leads carry Offers; buy leads carry BuyBids. The buy-lead endpoints mirror the sell-lead ones exactly.

Sell leads ​

MethodPathGatingNotes
GET/sell-leadsrequireAuthBrowse ACTIVE leads. Filters: productCategory, subProduct, state, district, trustTier, sort, page, limit.
POST/sell-leadsrequireActiveUser + sellQuotaCreate. Returns hints.{belowSalePrice, alreadyListed}.
GET/sell-leads/:idrequireAuthDetail + offers; identity masking for Light Users.
PATCH/sell-leads/:idrequireActiveUser + ownership + sellQuotaEdit once (editedOnce=true); second edit → 409.
DELETE/sell-leads/:idrequireActiveUser + ownership + sellQuotaSoft close.
POST/sell-leads/:id/viewrequireAuth (Active)Record a deduped LeadView; consumes 1 view quota. Idempotent.
POST/sell-leads/:id/offersrequireActiveUser + engagementQuota + mustHaveViewedCreate offer.
POST/sell-leads/:id/bookrequireActiveUser + engagementQuotaWraps view + offer in one call.
POST/sell-leads/:id/offers/:offerId/counterownership(lead)Owner counters a PENDING offer (48h timer).
POST/sell-leads/:id/offers/:offerId/respondownership(offer)Offeror responds to a COUNTERED offer: { decision: "accept"|"reject" }.
POST/sell-leads/:id/offers/:offerId/acceptownership(lead)Owner accepts (partial multi-accept).
POST/sell-leads/:id/offers/:offerId/rejectownership(lead){ reason? }.
POST/sell-leads/:id/offers/:offerId/withdrawownership(offer)Offeror withdraws PENDING or COUNTERED.
POST / DELETE/sell-leads/:id/shortlistrequireAuthAdd / remove shortlist (idempotent).

Buy leads ​

Same shape, with bids instead of offers:

MethodPathGating
GET/buy-leadsrequireAuth
POST/buy-leadsrequireActiveUser + buyQuota
GET/buy-leads/:idrequireAuth (buyer identity masked from non-owners)
PATCH / DELETE/buy-leads/:idrequireActiveUser + ownership + buyQuota
POST/buy-leads/:id/viewrequireAuth (Active)
POST/buy-leads/:id/bidsrequireActiveUser + engagementQuota + mustHaveViewed — sellerBatchId required (a bid names the seller's lot)
POST/buy-leads/:id/bookrequireActiveUser + engagementQuota
POST/buy-leads/:id/bids/:bidId/{counter,respond,accept,reject,withdraw}as sell side
POST / DELETE/buy-leads/:id/shortlistrequireAuth

Key business rules ​

Engagement gate (must view first) ​

POST /:id/offers and /:id/bids reject with 422 MUST_VIEW_FIRST unless a LeadView row exists for (userId, leadId). The natural UI flow (Open lead → detail → Make Offer) records the view; /book and Buy Now auto-record it.

Offer / bid state machine ​

PENDING → ACCEPTED | REJECTED | COUNTERED | WITHDRAWN; COUNTERED → ACCEPTED | REJECTED (or expiry) | WITHDRAWN. No counter-of-counter — an owner counters each engagement once. A COUNTERED engagement past its 48h counterValidUntil is auto-rejected by the hourly expire-counters job.

Partial multi-acceptance ​

Max 2 acceptances per lead; total quantityAccepted ≤ quantityKg. The lead auto-closes when either cap is hit and auto-rejects the remaining PENDING/COUNTERED engagements.

Contact sharing ​

On ACCEPTED, both parties get the counterparty's phone (in-app + SMS) and an Order is materialised. Phone numbers never appear in list/browse responses.

Identity visibility ​

  • Light Users see product, quantity, price, location — not names/phones/roles.
  • On sell leads, an Active User sees the seller name/role only if they are the seller or have a prior ACCEPTED deal.
  • On buy leads, the buyer's identity stays hidden until the caller's own bid is ACCEPTED.

Quota costs ​

ActionAxis
Post / edit / delete own leadPost (sellLeadsUsed / buyLeadsUsed)
Open someone else's lead (/view)View (deduped lifetime)
Place offer / bidEngagement (named after the lead's polarity)
Counter / respond / withdraw / accept / rejectnone

Internal technical documentation — Cropto