Overview
A fulfillment is one batch of items going to the customer by one method — a parcel from a warehouse, a digital download, or an order waiting at a pickup counter. An order gets one fulfillment per combination of stock location and delivery method, so an order sourced from two warehouses has two. The word is deliberately broader than “shipment”. A digital download has no carrier, no address and no package, and click-and-collect ships nowhere at all — a fulfillment covers all of them without dragging along fields that make no sense for most.Fulfillment attributes
Statuses
A fulfillment’s status says what you did with the parcel — nothing else.
Statuses only move forward: once something has gone out it stays gone out, and a later payment problem changes the order’s payment status rather than the fulfillment.
Whether a fulfillment can go out — the order is paid, nothing is on backorder — is checked when you fulfill it, and the answer comes back with a reason. It is not baked into the status, so a refund never moves a parcel backwards. Staff who need to ship against an invoice can pass
force.
Where the parcel actually is
One fulfillment can travel as several parcels — a gym machine that arrives in three boxes, or a pallet moving under one freight PRO number. Each is a delivery: its own tracking number, its own carrier, its own journey. A delivery’sstatus is what the carrier reports, and it never changes the
fulfillment’s own status:
Keeping the two apart is the point. A parcel that bounces still shows
fulfilled, because you did hand it over; the trouble shows up as
return_to_sender on that delivery and you decide what to do about it.
The fulfillment rolls up from its deliveries: it becomes delivered when
every consignment has arrived, so a machine that arrives in three boxes is
only delivered when the third one lands. That rollup is recomputed whenever
deliveries are added, removed or corrected — including when a tracking number
is fixed, which starts that consignment’s journey over.
delivered_at is what return windows and the EU withdrawal period count from,
so it records when the carrier says the parcel arrived rather than when you
heard about it.
Carriers report this through their provider’s webhook, matched to the delivery
by tracking number. With no carrier integration, staff mark receipt by hand —
markDelivered in the Admin SDK, on a single delivery or the whole
fulfillment — which is also how pickup orders get closed out.
A fulfillment keeps tracking and tracking_url as a summary of its first
consignment, so a storefront showing one tracking link needs no changes.
Shipping labels
Postage is its own record. A label is either purchased through a carrier integration or uploaded — bought elsewhere and recorded so the PDF and its cost live with the consignment. Buying one mints the delivery it covers, so you never enter the tracking number yourself. The file is fetched into Spree’s own storage right after purchase, so a merchant can reprint after the carrier’s link has expired or the integration has been disconnected. A label’scost is what you paid the carrier. It is admin-only accounting
data and never appears in a store API response or on the customer’s order.
A purchased label is given back by refunding it with the carrier; an uploaded
one has nothing to refund, so it is deleted instead. Either way the consignment
it minted goes with it only while the parcel never moved — once it is on its
way, that journey is a fact and stays.
documents, each with a kind and a url.
Delivery types
Every delivery method has a type that decides how it behaves:
This is what lets a store sell a downloadable album and a vinyl record in the same order without any special handling in your storefront: the download is ready immediately, the record gets a tracking number.
Choosing delivery at checkout
Each fulfillment on a cart offers delivery rates. The customer picks one per fulfillment.Pickup
For pickup methods, ask the API where the customer can collect:Managing fulfillments
How delivery is set up
Which methods a customer is offered, what they cost and where they reach is configured separately — see delivery setup.Adding your own statuses
A made-to-order business — print on demand, furniture built after purchase — has a stage between “order placed” and “handed to the carrier”. There are two ways to model it, and the lighter one is usually right. Track production in your own model, gate handover with a hook. A production pipeline usually has more resolution than one word — queued, printing, quality check, packed — and that detail belongs in your own tables or your integration’s metadata, not in the parcel’s lifecycle vocabulary. What core needs to know is only whether the parcel may go out yet, and that is exactly what the fulfill workflow’s validate hook expresses:server/config/initializers/spree.rb
unfulfilled while you build the thing; your storefront
renders “we’re making your furniture” as presentation, the same way pickup
orders render fulfilled as “ready for pickup”.
Add a real status when merchants need to act on the stage. If staff filter
by it, reports group by it, or webhooks fire on entering it, make it a
first-class status:
server/config/initializers/spree.rb
in_production? predicate, the .in_production scope and
a valid value. Moving into it is deliberately not declarative — write a small
workflow, which is also where the printer submission or the workshop handoff
belongs:
server/app/workflows/my_app/fulfillments/start_production.rb
in_production fulfillment can still be fulfilled or
canceled. Statuses are additive only — core workflows guard on core statuses,
so removing one would silently break them — and confirmed receipt still only
follows handover.
Events
Fulfillments publish events —fulfillment.created, fulfillment.fulfilled, fulfillment.delivered, fulfillment.canceled — which also reach webhooks. Use them to notify a customer, push to a warehouse system, or send tracking emails.
Related
- Orders — how delivery status rolls up
- Inventory — stock locations and availability
- Returns, Exchanges & Claims — items coming back

