Customer collection

Updated May 31, 20265 min read

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, prefixed COL_ (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 (OTP or NONE), status (pending, verified, or overridden), and timestamps.
  • pack_id. The pack that created this collection.
  • customer_collection_schedule. The customer's preferred pickup window, copied from the fulfillment order.
  • notes and carriyo_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
StatusMeaning
openCreated but not yet at the collection point. The warehouse is still preparing the parcel or it's in transit.
ready_to_collectAt the collection point. The customer can be notified and verified.
collectedHanded over to the customer. Terminal: success.
expiredThe customer didn't collect within the configured window. Recoverable via unexpire if they show up late.
cancelledEither explicitly cancelled or auto-cancelled after expiry. Terminal.

Transitions

FromToTrigger
openready_to_collectPOST /collections/{id}/ready. Store staff confirms the parcel is at the counter.
ready_to_collectcollectedPOST /collections/{id}/verification/verify-and-collect. Successful OTP verification, or staff override.
ready_to_collectexpiredAuto-expire scheduler (see below).
ready_to_collectopenPOST /collections/{id}/reopen. Sent back to the warehouse.
expiredready_to_collectPOST /collections/{id}/unexpire. Customer turned up late, give them another window.
expiredcancelledAuto-cancel scheduler (see below).
open, ready_to_collect, expiredcancelledPOST /collections/{id}/cancel. Manual cancellation with optional cancellation_reason.
collected(terminal)Terminal.

OTP verification

Handover is gated on OTP verification by default. The flow:

  1. Send OTP. POST /collections/{id}/verification/send-otp generates a 6-digit code, hashes it (SHA-256), and dispatches it to the customer's email. The response includes masked_email and otp_expires_at so the staff can see where the code went without leaking the address.
  2. Verify and collect. POST /collections/{id}/verification/verify-and-collect accepts the OTP from the customer. On match, the collection moves to collected.
  3. Override. Same endpoint with override = true and no OTP. For staff exceptions where the customer can prove identity but can't access the OTP. Recorded as verification.status = overridden for 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_collect for 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:

SettingDefaultEffect
customerCollectionAutoExpireEnabledoffWhen on, a ready_to_collect collection automatically transitions to expired after customerCollectionAutoExpireDays days (default 7).
customerCollectionAutoCancelEnabledoffWhen 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 eventLine-item effect
Collected (OTP verified or overridden)If all collections for that line item are now terminal, the line item moves to closed.
CancelledSame 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 = COLLECTION fulfillment 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 COLLECTION order.