Skip to main content

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’s status 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’s cost 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.
Some carriers also produce paperwork beside the label — a commercial invoice or a customs declaration for anything crossing a border. Those come back on the fulfillment’s 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.
Rates carry more than a price — carrier, service level and estimated delivery date — so you can show “DHL Express, arrives Tuesday” instead of just a number.

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
The fulfillment stays 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
That gives you the 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
Core’s own actions keep working on your status without overrides: the guards ask whether the parcel has already gone out, not whether it holds one of the built-in values, so an 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 eventsfulfillment.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.