Customer collection
A customer collection is the entity that represents a customer picking up their order in person: the "C" in click-and-collect / BOPIS / buy online, pick up in store. It tracks the parcel from when it's ready at the collection point through to handover. OTP verification confirms who physically receives the goods.
A collection is the collect-mode counterpart of a shipment. A ship-mode order ends in a shipment that a carrier moves to the customer; a collect-mode order ends in a collection the customer picks up in person. Both are the terminal fulfillment artifact for their delivery mode, and an order produces one or the other, never both.
A customer collection always belongs to a fulfillment order with
delivery_method = COLLECTION. The two entities are split
because they answer different questions:
- The fulfillment order says which lines, from which location, going to which collection point.
- The customer collection says the parcel is ready, the customer has been verified, the handover happened.
The model
A collection carries:
collection_id. Unique identifier, prefixedCOL_(e.g.COL_58).status. The lifecycle state (see below).location_id. The pickup location.address. The collection point address.packages. The parcels available for pickup, copied from the pack that created this collection.customer. Name, phone, and email of the person collecting. The email is where the OTP is sent.verification. The OTP verification state:method(OTPorNONE),status(pending,verified, oroverridden), and timestamps.pack_id. The pack that created this collection.customer_collection_schedule. The customer's preferred pickup window, copied from the fulfillment order.notesandcarriyo_metadata. Free-text notes and custom key-value pairs.
Creation triggers
A customer collection is created automatically when the
Fulfillment App is in use and the fulfillment order's
delivery_method is COLLECTION.
The two collection scenarios from the Fulfillment orders page both produce one collection:
- Local collection. The fulfillment location is the collection point. No shipment is created; the collection is the only fulfillment artifact.
- Remote collection. The fulfillment location is a warehouse and the collection point is a different store. A shipment moves the parcel between them, and a collection is created at the destination store for the handover.
Lifecycle states
A customer collection moves through five states:
open ─▶ ready_to_collect ─▶ collected
│
├─▶ expired ─▶ cancelled (auto)
│ ▲
│ │ unexpire
▼ │
cancelled (manual)
▲
└── reopen ──▶ open
| Status | Meaning |
|---|---|
open | Created but not yet at the collection point. The warehouse is still preparing the parcel or it's in transit. |
ready_to_collect | At the collection point. The customer can be notified and verified. |
collected | Handed over to the customer. Terminal: success. |
expired | The customer didn't collect within the configured window. Recoverable via unexpire if they show up late. |
cancelled | Either explicitly cancelled or auto-cancelled after expiry. Terminal. |
Transitions
| From | To | Trigger |
|---|---|---|
open | ready_to_collect | POST /collections/{id}/ready. Store staff confirms the parcel is at the counter. |
ready_to_collect | collected | POST /collections/{id}/verification/verify-and-collect. Successful OTP verification, or staff override. |
ready_to_collect | expired | Auto-expire scheduler (see below). |
ready_to_collect | open | POST /collections/{id}/reopen. Sent back to the warehouse. |
expired | ready_to_collect | POST /collections/{id}/unexpire. Customer turned up late, give them another window. |
expired | cancelled | Auto-cancel scheduler (see below). |
open, ready_to_collect, expired | cancelled | POST /collections/{id}/cancel. Manual cancellation with optional cancellation_reason. |
collected | (terminal) | Terminal. |
OTP verification
Handover is gated on OTP verification by default. The flow:
- Send OTP.
POST /collections/{id}/verification/send-otpgenerates a 6-digit code, hashes it (SHA-256), and dispatches it to the customer's email. The response includesmasked_emailandotp_expires_atso the staff can see where the code went without leaking the address. - Verify and collect.
POST /collections/{id}/verification/verify-and-collectaccepts the OTP from the customer. On match, the collection moves tocollected. - Override. Same endpoint with
override = trueand no OTP. For staff exceptions where the customer can prove identity but can't access the OTP. Recorded asverification.status = overriddenfor audit.
Constraints baked into the verification flow:
- 6 digits.
- 5 minute expiry from send.
- 5 attempts maximum before the OTP is invalidated; the customer must request a new one.
- 60 second cooldown between resends.
- The collection must be in
ready_to_collectfor both send and verify-and-collect.
The OTP itself is never stored, only its SHA-256 hash. The plaintext is sent to the customer's email and exists only in the verification request body while it is compared.
Auto-expiry and auto-cancellation
Two tenant-level switches govern the unattended timer behavior. Configure them in the Dashboard under Order management settings:
| Setting | Default | Effect |
|---|---|---|
customerCollectionAutoExpireEnabled | off | When on, a ready_to_collect collection automatically transitions to expired after customerCollectionAutoExpireDays days (default 7). |
customerCollectionAutoCancelEnabled | off | When on, an expired collection automatically transitions to cancelled after customerCollectionAutoCancelDays days. Has no effect unless auto-expire is also on. |
Auto-expire schedules its own follow-up cancel job at the
moment of expiry. If the customer turns up between expiry and
auto-cancel, unexpire puts the collection back into
ready_to_collect and both schedules are dropped.
Both timers are tenant-wide policy. There is no per-order override in the API today.
How the order closes after handover
When a collection reaches a terminal state (collected or
cancelled), Carriyo closes the parent fulfillment-order line
items. The cascade:
| Collection event | Line-item effect |
|---|---|
| Collected (OTP verified or overridden) | If all collections for that line item are now terminal, the line item moves to closed. |
| Cancelled | Same check: if all collections for the line item are terminal, it moves to closed. |
After the line item closes, the fulfillment order status is recomputed from its line items, then the order status is recomputed from its fulfillment orders.
A line item can reference more than one collection when fulfillment was split across packages or scheduled across multiple pickups. The line item only closes when every collection has reached a terminal state. An order stays open while any customer still has goods to collect.
Lookup endpoints vs full record
Collections are stored with several lookup keys (by order, fulfillment order, pack, and shipment). The list endpoints return sparse lookup records with summary fields only. They are useful for answering "which collections exist for this order?" but don't include packages, verification details, or timestamps.
Always call GET /orders/collections/{collectionId} for the
complete record. A typical integration pattern is to list by
order first, then fetch each collection individually.
How it fits with other modules
- Fulfillment orders.
Every customer collection belongs to a
delivery_method = COLLECTIONfulfillment order. - Order lifecycle. Successful collection is one of the two paths (alongside shipment delivery) that close an order.
- Locations. The collection point is a Location with a customer-collection capability.
- Click and collect.
The upstream checkout flow that produces a
COLLECTIONorder.