Skip to content

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.

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.

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.

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_event reads vars.seller_responded, the payload of the event: { "sellerId": "sel_kettle", "decision": "accepted", "reason": "…" }. decision is accepted or rejected; reason is optional. Only a pending part changes, so a repeated answer is ignored. A payload without sellerId and a valid decision fails the step.
  • decision: expire_pending runs when the wait times out and rejects every pending part with the reason No 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.

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.

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.

commerce.payout runs after the return window. For each accepted third-party seller it schedules one payout:

  • gross: the seller’s subtotal.
  • refunded: the succeeded refunds so far, shared pro rata: refunds × gross / order total (never more than gross).
  • 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.

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.