Marketplace
Sellers, splitting an order by seller, seller acceptance, partial capture and payouts net of commission on the marketplace flow.
A marketplace order is sold by the operator and by third-party sellers at once. The built-in
tpl-marketplace-order flow (see Built-in flows) scores the order’s fraud risk (a high score goes to a
Trust & safety approval, and a refusal declines the order), authorizes the card, splits the order into one
part per seller, waits for the sellers that must accept, ships and captures what was accepted, and
schedules each seller’s payout net of commission. The nodes are in the node reference:
commerce.split_by_seller, commerce.seller_accept and commerce.payout.
Sellers and first party
Section titled “Sellers and first party”A seller has a commission (a fraction; 0.12 is 12%), whether it accepts orders automatically,
and whether it is active. The back office lists sellers under Marketplace sellers. The
workspace starts with two:
| Seller | Commission | Accepts | Product |
|---|---|---|---|
sel_brightside (Brightside Audio) |
12% | Automatically | HP-200 |
sel_kettle (Kettle & Co) |
15% | When asked, per order | KET-01 |
A line’s seller is, in order: lines[].sellerId on the order input, the product’s seller, else
first party. A first-party line is the operator’s own: it needs no acceptance and has no payout.
The split
Section titled “The split”commerce.split_by_seller records one seller order per order and seller, with that seller’s
lines and subtotal. First-party lines and sellers that accept automatically start accepted; the others
start pending. The node takes accepted when nothing is pending and awaiting otherwise, and sets
vars.sellers and vars.pendingSellers. A seller that doesn’t exist or is inactive fails the step
(it isn’t retried), and the order rolls back. A retried step changes nothing: there is one seller order per order and
seller.
Acceptance loop
Section titled “Acceptance loop”On awaiting, the flow sends commerce.notify with to: seller (the sellers still to answer), then
runs a loop of at most 10 passes. Each pass waits 2 days for
seller.responded and records it with commerce.seller_accept:
decision: from_eventreadsvars.seller_responded, the payload of the event:{ "sellerId": "sel_kettle", "decision": "accepted", "reason": "…" }.decisionisacceptedorrejected;reasonis optional. Only apendingpart changes, so a repeated answer is ignored. A payload withoutsellerIdand a validdecisionfails the step.decision: expire_pendingruns when the wait times out and rejects every pending part with the reasonNo response.
The node’s branch:
| Output | When |
|---|---|
pending |
Some seller has still to answer; the flow loops to wait again |
accepted |
No one is pending and every seller accepted |
partial |
No one is pending, some accepted and some rejected |
rejected |
No one is pending and none accepted; the order fails with “No seller accepted” and the authorization is voided |
If the loop reaches its limit with sellers still pending, the order fails with “Sellers did not
answer”. After accepted or partial the flow reserves stock, ships and captures.
Sending seller.responded
Section titled “Sending seller.responded”Any way to deliver an event works. On the order
page, a seller.responded wait lists each pending seller with Accepts and Rejects buttons.
Through the REST API:
curl -X POST https://orderloop.app/api/v1/orders/ord_…/events \ -H "Authorization: Bearer $ORDERLOOP_TOKEN" -H 'content-type: application/json' \ -d '{ "event": "seller.responded", "payload": { "sellerId": "sel_kettle", "decision": "accepted" } }'A seller’s own system should use the signed event
endpoint, POST /api/v1/webhooks/events, with the same event and payload. Give each seller
its own integration limited to seller.responded, so a
seller’s key can’t deliver anything else.
The loop waits for one seller.responded per pass. A second response that arrives while the flow is
between passes (recording the first, or starting the next wait) finds no node waiting: the response
says "delivered": false (with status 409 on the signed endpoint). Retry it; once the next wait
opens it is delivered.
Partial acceptance and capture
Section titled “Partial acceptance and capture”On partial, commerce.seller_accept drops the rejected sellers’ lines from the order and
re-totals it (vars.rejectedSellers lists them). The card was authorized for the full order, so
commerce.capture_payment captures the lesser of the authorization and the new order total and
releases the rest. The customer is charged only for what ships.
Payouts
Section titled “Payouts”commerce.payout runs after the return window. For each accepted third-party seller it schedules one
payout:
gross: the seller’ssubtotal.refunded: the succeeded refunds so far, shared pro rata:refunds × gross / order total(never more thangross).commission:(gross − refunded) ×the seller’s commission.net:gross − refunded − commission.
Amounts are in the order’s currency and rounded to cents. For one HP-200 at $129 from Brightside
Audio (12%) with no refund: gross 129.00, commission 15.48, net 113.52. The node sets
vars.commission (the total) and vars.payouts ([{ sellerId, net }]), and sends the
order.payout_scheduled outbound webhook. With no third-party seller it
schedules nothing. The order page shows the Sellers and Seller payouts ledgers.
Rollbacks and cancels
Section titled “Rollbacks and cancels”commerce.split_by_seller and commerce.payout are compensable. A failed order’s rollback cancels
the seller orders (pending and accepted become cancelled; a rejected one stays rejected) and
reverses scheduled payouts (status reversed). Cancelling an order does the same. A payout already paid is left alone.