Fulfillment & money flow

What happens after an order is paid: how the item reaches the buyer, and how the money moves. Covers all three fulfillment methods — shipping (a carrier), same_day (an on-demand courier), meetup (in person) — and every cancel and refund path.

Four layers

LayerColumnSource of truth
Order stateorders.statusCüte's order machine
Payment statepayments.statusCüte, informed by the payment provider
Fulfillment statefulfillments.statusCüte, informed by the delivery gateway or the handoff
Payout stateorders.payout_statusCüte's held-funds lifecycle

The machine fires normalized events; webhook handlers stamp payments.provider_status_raw and fulfillments.provider_status_raw, so the provider's own words survive next to our reading of them.

The provider behind each layer is chosen per market — Stripe or Pagar.me for payment, Sendcloud or Melhor Envio for labels, a courier gateway for same-day. Nothing on this page is specific to one of them.

When the label is bought

When the seller starts shipping — not at payment_confirmed.

payment_confirmed
  → fulfillments.status = awaiting_seller
  → the buyer's shipping fee is held alongside the item price
  → no label yet

seller starts the shipment
  → createShipment → purchaseLabel → generateLabel → getLabel
  → fulfillments.gatewayShipmentId and labelUrl set
  → fulfillments.status = label_generated
  → the PDF opens
  → orders.shippedAt is STILL null — printing is not shipping

seller hands the parcel over
  → the carrier scans it
  → webhook → `seller_shipped`
  → orders.shippedAt = now, fulfillments.status = posted
  → the buyer sees "in transit"

Why printing is not shipping. If shippedAt were set at print time, the ship-deadline guard would stop protecting the buyer: a seller who prints and never posts would hold the money indefinitely. Tying shippedAt to the carrier's own scan keeps the deadline honest.

Why not at payment time. Labels are bought from a funded gateway balance, so buying early locks money against orders that may never ship — and cancellations between payment and dispatch are common. Not every gateway refunds an unused label. Buying at dispatch is also what Mercado Livre, Etsy and Shopify do.

Each step is a separately journaled step in the durable saga, which is why they are separate gateway methods rather than one call: purchaseLabel is the balance debit and has to be independently resumable.

Happy path — shipping

Happy path — same-day

Happy path — meetup

The buyer's confirmation is the handoff. There is no separate seller step: one tap completes the order and releases the funds.

A meetup has no shipping to be late on, so the ship deadline never applies to it.

What the buyer pays, and where it goes

Buyer's total = item price + buyer protection fee + delivery fee (zero for meetup).

SliceGoes toWhen
Item priceThe seller's payment accountAt capture, into the held bucket until release
Buyer protection feeCüteAt capture, released immediately
Delivery feeCüteAt capture; consumed by the label at dispatch. Refunded if the order never ships

Cancellations and refunds

ReasonOrder statusLabel boughtBuyer gets backSeller balance
payment_failedpending_paymentnonothing was captured—
payment_expiredpending_paymentnonothing was captured—
buyer_cancel before paymentpending_paymentnonothing was captured—
buyer_cancel after paymentactive, not shippednoeverythingheld funds reversed
seller_cancelactive, not shippednoeverythingheld funds reversed
admin_cancelactive, not shippednoeverythingheld funds reversed
ship_deadline_expiredactive, not shippednoeverythingheld funds reversed
dispute_resolved_refunddisputedyes, consumeditem + protection fee. Not the delivery feeheld funds reversed
dispute_resolved_sellerdisputedyesnothingheld funds released to the seller

For same-day, the delivery fee is refundable only while the courier has not been dispatched. After dispatch the courier is owed for the run either way, so a refund is the buyer's total minus the courier cost — an admin decision. For meetup there is no delivery fee to argue about.

orders.payout_status

StageValueMeaning
Just creatednot_readyPayment has not confirmed; nothing is held
Payment confirmedholdThe item price sits in the seller's held bucket. Not withdrawable
Acceptance window passed, or the buyer acceptedreleasableQualifies for release
ReleasedreleasedMoved held → available. The seller can request a payout
Dispute openblockedFrozen until the dispute closes

Deadlines

TimerWindowCronFires
PIX charge (BR)30 minutes (pixChargeExpiryMs)expire-pending-paymentspayment_expired → cancelled
Card anti-fraudprovider-managed—payment_confirmed or payment_failed
Ship deadline7 days from payment_confirmed (shipDeadlineMs)overdue-ordersship_deadline_expired → cancelled + refunded. Guarded by notShipped
Buyer acceptance2 days from delivery or handoff (acceptanceWindowDays)auto-accept-ordersauto_accept_timeout → completed, funds released. Guarded by noActiveDispute

Every window is a market config value, not a constant in the code.

Who can do what

TransitionEventActorGuard
→ pending_paymentorder createdsystemseller eligible
pending_payment → activepayment_confirmedwebhook—
pending_payment → cancelledpayment_failed / payment_expired / buyer_cancelwebhook / cron / buyer—
active (no state change)seller_readyseller / system—
active (no state change)label_purchased / label_generated / seller_shippedwebhook / systemmethodTakesEvent (shipping)
active (no state change)courier_dispatchedwebhook / systemmethodTakesEvent (same-day)
active (no state change)courier_in_transitwebhook / systemmethodTakesEvent (shipping, same-day)
active (no state change)fulfillment_failed / fulfillment_pausedwebhook / system—
active → awaiting_buyer_confirmationdelivery_confirmedwebhook / systemmethodTakesEvent (shipping, same-day)
active → awaiting_buyer_confirmationhandoff_confirmedsellermethodTakesEvent (meetup)
active → cancelledship_deadline_expiredcron / system / adminnotShipped
active → cancelledbuyer_cancel / seller_cancel / admin_cancelbuyer / seller / admin—
awaiting_buyer_confirmation → completedbuyer_acceptedbuyer—
awaiting_buyer_confirmation → completedauto_accept_timeoutcron / systemnoActiveDispute
awaiting_buyer_confirmation → disputeddispute_openedbuyercanOpenDispute
disputed → cancelled / completedthe five closures, second firesystem (the saga)the matching settles* guard — see Dispute region

Failure modes worth knowing

  • The gateway balance is empty when the seller dispatches. The label purchase fails, the order stays active and fulfillments.status stays awaiting_seller. The seller sees an error; ops tops the balance up and the seller retries.
  • The label is generated but the carrier's scan webhook never arrives. fulfillments.status stays label_generated and shippedAt stays null, so the ship-deadline cron can still cancel and refund. An admin can advance it by hand.
  • The seller posts on the last day. The ship deadline runs from payment_confirmed, so a cron pass may land right after a genuine dispatch — which is exactly what the notShipped guard is for.
  • The buyer never confirms. Auto-accept fires after the acceptance window, unless the dispute region is active, in which case the payout stays blocked until it closes.