Held Carts
Hyvä POS is in closed beta
Hyvä POS is currently in a closed beta (pilot phase) with a small group of merchants. It is not yet generally available: the App Store release follows the pilot, and features and configuration may still change - possibly in backwards-incompatible ways - before the general release. Want to take part? Sign up at hyva.io/pos.
| Method | Route | Purpose |
|---|---|---|
GET |
/V1/pos/held-carts/location/:locationId |
The location's shared parked carts |
POST |
/V1/pos/held-carts |
Park a cart server-side |
POST |
/V1/pos/held-carts/:heldCartId/recall |
Claim a parked cart for this register |
DELETE |
/V1/pos/held-carts/:heldCartId |
Discard a parked cart |
DELETE |
/V1/pos/held-carts/uuid/:cartUuid |
Discard by cart UUID (when the numeric id is unknown, e.g. offline-created carts) |
All routes require the Hyva_Pos::locations ACL resource. A held cart is a parked in-progress sale: the cashier holds it on one register, any register at the same location can recall it and finish the sale. The server stores the full cart as an opaque JSON blob (cart_data) plus a summary row (customer name, item count, grand total) so registers can render the parked-carts list without deserializing every cart.
Sharing and the Enforcement Flag
Cross-terminal sharing is controlled by the Magento configuration flag Share Held Carts (hyva_pos/general/share_held_carts, Stores → Configuration → Hyvä POS). The flag is delivered to the app as share_held_carts in GET /V1/pos/config. When enabled, every terminal at a location syncs its parked carts through these routes; when disabled, holds stay local to the device.
The companion lock Lock Held Cart Sharing (hyva_pos_lock/enforcement/held_carts) is delivered as enforcement.held_carts in the same config payload. When set, the Magento value is enforced on every terminal and the corresponding POS setting becomes read-only on the device - a terminal cannot opt out of (or into) sharing locally.
List Held Carts for a Location
GET /V1/pos/held-carts/location/:locationId - all parked carts at a location, newest first. The POS polls this to populate the parked-carts sheet on every register. Optional query parameter pageSize (default 100).
Response - an array of held-cart rows:
[
{
"held_cart_id": 41,
"location_id": 3,
"staff_id": 12,
"staff_name": "Anna",
"terminal_id": null,
"cart_uuid": "8F5E2C7A-1B0D-4A6B-9E1F-3C2D4E5F6A7B",
"cart_data": "{\"id\":\"8F5E2C7A-...\",\"items\":[...]}",
"customer_name": "Jane Doe",
"item_count": 3,
"grand_total": 129.85,
"currency_code": "EUR",
"hold_comment": "Waiting on a size check",
"recalled_by_staff_id": null,
"recalled_at": null,
"created_at": "2026-08-12 10:41:03",
"updated_at": "2026-08-12 10:41:03"
}
]
cart_data is the serialized cart exactly as the app produced it. Treat it as a blob: its schema belongs to the POS app and changes between app versions. A row with a non-null recalled_by_staff_id has already been claimed by another register.
Park a Cart
POST /V1/pos/held-carts - saves a cart server-side. The POS calls this when the cashier taps Hold on an active sale.
Request - the row is wrapped in a heldCart key. location_id, cart_uuid, cart_data, customer_name, item_count, grand_total, and currency_code are what the app always sends; staff_id, staff_name, terminal_id, and hold_comment are optional:
{
"heldCart": {
"location_id": 3,
"cart_uuid": "8F5E2C7A-1B0D-4A6B-9E1F-3C2D4E5F6A7B",
"cart_data": "{\"id\":\"8F5E2C7A-...\",\"items\":[...]}",
"customer_name": "Jane Doe",
"item_count": 3,
"grand_total": 129.85,
"currency_code": "EUR",
"staff_id": 12,
"staff_name": "Anna",
"hold_comment": "Waiting on a size check"
}
}
Response: the saved row in the shape shown above, now carrying the server-assigned held_cart_id and timestamps. cart_uuid is the client-generated identity of the cart - re-posting the same UUID updates rather than duplicates.
Errors: 400 (CouldNotSaveException) when the row cannot be persisted.
Recall a Held Cart
POST /V1/pos/held-carts/:heldCartId/recall - claims a parked cart for the recalling register. The claim is atomic: the server sets recalled_by_staff_id and recalled_at in one statement, so two registers recalling the same cart at the same moment cannot both win.
Request:
Response: the full held-cart row including cart_data, which the recalling register deserializes back into an active sale.
Errors: 400 when the cart was already recalled by another register - surface this as "another register took this sale", not as a technical failure. 404 for an unknown id.
Discard a Held Cart
DELETE /V1/pos/held-carts/:heldCartId - deletes by server id. The POS calls this after a recalled cart completes checkout, and when a cashier explicitly discards a parked sale.
DELETE /V1/pos/held-carts/uuid/:cartUuid - deletes by the client-generated cart UUID. Used when the register never learned the server id - typically a cart parked while offline and synced later.
Response: true on success. Errors: 400 (CouldNotDeleteException) when the delete fails.