Fulfillment and Shipping
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.
These endpoints back the Pickups view (in-store pickup handoffs) and ship-to-customer fulfillment (rates, carriers, labels). All of them require the Hyva_Pos::locations ACL resource.
| Method | Route | Purpose |
|---|---|---|
GET |
/V1/pos/locations/:locationId/pickups |
Classified pickup queue for a location |
POST |
/V1/pos/orders/:orderId/pickup-completion |
Record the physical handoff |
POST |
/V1/pos/orders/:orderId/pickup-cancellation |
Cancel an uncollected pickup |
GET |
/V1/pos/products/:childSku/bundle-parents |
Reverse lookup: bundles containing a product |
POST |
/V1/pos/shipping-rates |
Live rates for a customer address |
GET |
/V1/pos/shipping-config |
Offline-cacheable flat-rate shipping methods |
GET |
/V1/pos/carriers |
Active tracking carriers for dispatch |
POST |
/V1/pos/orders/:orderId/ship-with-label |
Create a shipment and return printable label data |
GET |
/V1/pos/shipments/:shipmentId/shipping-label |
Carrier label for an existing shipment |
List Pickups
GET /V1/pos/locations/:locationId/pickups?filter=ready
Single backing query for the Pickups view. Returns every open order with an instore_pickup_* shipping method that has not shipped yet, scoped to the location's MSI source. Both storefront-placed pickups and POS-placed backorders surface here, distinguished by origin.
filter is one of ready, awaiting_stock, stale, or all (the default). Classification per order:
ready- every physical line is coverable from the on-hand quantity at the bound MSI source.awaiting_stock- at least one line cannot be covered yet.stale- not ready and older than the stale threshold (14 days), overridingawaiting_stock.
The response is deliberately thin - the endpoint earns its keep through joins searchCriteria cannot express (orders × shipments × source items × age), not through data shape. The app hydrates each visible row via the stock GET /V1/orders/:id:
[
{
"order_id": 812,
"order_increment_id": "000000812",
"origin": "online",
"classification": "ready",
"source_code": "amsterdam_store",
"created_at": "2026-08-10 09:15:00",
"expected_at": null
}
]
origin is online or pos_backorder. expected_at is a best-guess readiness ETA, null when the row is already ready or no projection is possible. A server-side cron re-checks backorder pickups every five minutes so rows flip to ready as stock arrives.
Complete a Pickup
POST /V1/pos/orders/:orderId/pickup-completion
Adobe's in-store-pickup module has no "picked up" concept - shipment creation is its de-facto fulfillment event. This endpoint records the physical handoff atomically: it creates the shipment if none exists (deducting from the pickup location's MSI source), inserts the hyva_pos_pickup_completion audit row, appends a status-history comment (Picked up by {name}. {comment}), and optionally flips the order status to the location's configured post-pickup status.
pickedUpBy is required. sendNotification (default true) controls the customer-facing pickup-confirmation email; the audit row and comment are committed before the email fires, so a mail transport failure never rolls back the handoff record.
Error behavior is deliberately specific so the cashier gets something actionable instead of Magento's generic "Could not save a shipment". All are 400 LocalizedException responses:
Pickup completion requires a collector name.Order %1 is not a pickup order (shipping method: %2).Order %1 has already been picked up.- duplicate completion.Stock not assigned at %1 for: %2. Add stock at this location first, then complete the pickup.- one or more SKUs have no source-item row at the pickup source;%2lists them.Insufficient stock at %1 for: %2.- the source-item row exists but quantity is short;%2lists each SKU with(need N, have M).Could not create the shipment for order %1: %2- catch-all that surfaces the deepest inner exception message.
Orders pre-shipped by admin's "Notify Order is Ready for Pickup" button are tolerated - the shipment step is skipped and only the completion record and comment are written.
Cancel a Pickup
POST /V1/pos/orders/:orderId/pickup-cancellation
For pickups the customer did not (or will not) collect. Routes through Magento's OrderManagementInterface::cancel, which handles the state transition and restores MSI reservations, then stamps a status-history comment: Pickup cancelled at {location}. Reason: {reason}.
reason is required. Refused with a 400 when the order is not a pickup order, has already been picked up (issue a refund instead), or has already shipped (process a refund/return instead) - shipped goods need the return flow, not a cancel.
Bundle Parent Lookup
GET /V1/pos/products/:childSku/bundle-parents returns the bundle products that contain :childSku as a selection, so the app can warn about "Ship Together" bundles before MSI validation rejects a partial fulfillment. Magento core has no equivalent REST surface.
Shipping Rates
POST /V1/pos/shipping-rates
Live rates for ship-to-customer lines. The POS keeps cart state locally and places orders directly, so there is no Magento cart to estimate against - the service builds a transient quote, runs the standard rate collector (table rates, UPS, FedEx, any configured carrier), and discards the quote.
{
"customerId": 42,
"addressId": 118,
"itemsJson": "[{\"sku\":\"WSH12\",\"qty\":1,\"price\":45.0}]"
}
addressId must be an address entity id belonging to the customer. The response is an array of rate rows; carrier_code + method_code compose the carrierCode_methodCode value the order placement stamps onto its shipping assignment:
[
{
"carrier_code": "ups",
"method_code": "GROUND",
"carrier_title": "United Parcel Service",
"method_title": "Ground",
"amount": 12.5,
"currency": "EUR",
"estimated_days": 3,
"tax_rate": 21.0,
"tax_amount": 2.63,
"tax_class_id": 4
}
]
Tax fields are resolved server-side against tax/classes/shipping_tax_class and the destination tax zone, honoring tax/calculation/shipping_includes_tax, so the client never reproduces tax math. They are null only on module versions that pre-date them.
Shipping Config and Carriers
GET /V1/pos/shipping-config returns the offline-cacheable shipping-method list: flat-shape carriers (flatrate, freeshipping) with carrier_code, method_code, titles, amount, currency, and the same server-resolved tax_rate / tax_class_id. The app replaces its local settings.shippingCarriers list from this on every Settings → Shipping open; live rates from pos/shipping-rates override it per checkout.
GET /V1/pos/carriers returns the dispatch-side list - carriers activated under Stores → Configuration → Sales → Delivery Methods, as {"code": "dhl", "title": "DHL", "sort_order": 10} rows sorted by sort_order. These are the values written to shipment_track.carrier_code when shipping with tracking; carriers without a configured title are filtered out. PRO installs auto-sync this list and lock the on-device editor.
Ship With Label
POST /V1/pos/orders/:orderId/ship-with-label
Pack-and-print action for the Pickups view. Creates a Magento shipment for the selected items and returns label data in the same round-trip, so the app can route the print job immediately.
{
"itemsJson": "[{\"order_item_id\":12,\"qty\":1}]",
"trackingNumber": "3STEST1234567",
"carrierCode": "postnl",
"trackingTitle": "PostNL Pakket"
}
An empty itemsJson array means "ship everything still shippable". carrierCode and trackingTitle land on the shipment_track row; when omitted they fall back to the order's shipping method and description - pass them explicitly when recording real carrier data. Response:
{
"order_id": 812,
"shipment_id": 402,
"shipment_increment_id": "000000402",
"label_format": "escpos",
"label_data": "G0AbYQEuLi4=…(base64)",
"tracking_number": "3STEST1234567"
}
label_format declares the print pipeline: escpos (thermal printer bytes - the default packing-slip-style label with order barcode), pdf (carrier-API label documents), text, or zpl (raw Zebra). label_data is always base64-encoded. Real carrier-API labels come from carrier integration modules that override the label generation.
Shipping Label for an Existing Shipment
GET /V1/pos/shipments/:shipmentId/shipping-label is read-only - it never creates shipments. Resolution is delegated to the registered shipping-label provider pool, so it returns 404 (NotFoundException) until a carrier bridge module is installed, and 404 (NoSuchEntityException) when the shipment does not exist. The response shape is the same label object as ship-with-label.