Shipping and Fulfillment
Shipping profiles and methods with their rates, routing, fulfillment jobs, and deliveries and pickups
Getting goods to people uses five records:
- a shipping profile is a shipping label for products that ship the same way, such as “standard” or “fragile”;
- a shipping method is the customer’s choice at checkout (a delivery, or a pickup at one location) with its rates inside it;
- fulfillment routing is the store’s rules for which location fulfills a delivery;
- a fulfillment job is one location’s work on all or part of one order delivery, or one rental replacement;
- a fulfillment is a set of units that leave together: a delivery that is dispatched and delivered, or a pickup that is collected.
Shipping Profiles
/v1/stores/{store_id}/shipping-profiles sdk.store.shippingProfile.create() /v1/stores/{store_id}/shipping-profiles sdk.store.shippingProfile.find() /v1/stores/{store_id}/shipping-profiles/{id} sdk.store.shippingProfile.update() /v1/stores/{store_id}/shipping-profiles/{id}?expected_updated_at= sdk.store.shippingProfile.delete() A profile is { id, store_id, key, created_at, updated_at }. Every physical variant names one.
Items of different profiles go in separate deliveries, each with its own method and rate.
Shipping Methods And Rates
/v1/stores/{store_id}/shipping-methods sdk.store.shippingMethod.create() /v1/stores/{store_id}/shipping-methods sdk.store.shippingMethod.find() /v1/stores/{store_id}/shipping-methods/{id} sdk.store.shippingMethod.update() /v1/stores/{store_id}/shipping-methods/{id}?expected_updated_at= sdk.store.shippingMethod.delete() const courier = await sdk.store.shippingMethod.create({
store_id: storeId,
id: crypto.randomUUID(),
key: "courier",
blocks: [{ id: crypto.randomUUID(), key: "name", type: "localized_text", value: { en: "Courier", de: "Kurier" } }],
type: { type: "delivery" },
tax: { type: "category", tax_category_id: standardTax.id },
rates: [
{
id: crypto.randomUUID(),
zone_id: domestic.id,
shipping_profile_id: standardProfile.id,
conditions: [],
pricing: { type: "flat", amount: 1_000, free_above_subtotal: 10_000 },
delivery_estimate: { min_business_days: 1, max_business_days: 3 },
starts_at: null,
ends_at: null,
},
],
status: { type: "active" },
});
- A method is a
delivery, or apickupat one store location. A location offers pickup exactly when an active pickup method names it. - Rates live inside the method and are written with it, up to 500. Each rate names a zone and a profile, optional conditions (sales channels, customer groups or a whole customer group offering, companies, company locations, subtotal or weight bounds), a flat or weight-tiered price with an optional free-above subtotal, and an optional delivery estimate.
- Two rates that could match the same cart are refused when saved: same zone and profile, overlapping dates, and conditions that could all hold together.
- Fixed amounts are in the currency of the rate’s zone’s market. Subtotal bounds use the delivery’s merchandise after item and order discounts, before delivery discounts and without separately added tax.
- A unit’s weight is its kit items’ weights times their quantities. A rate that uses weight, through
a condition or tiers, needs every unit’s weight: with an unknown weight that rate is left out of the
quote’s offers and listed as unavailable with
SHIPPING.WEIGHT_UNKNOWN, never counted as zero. taxis a taxcategory, orfollow_goods: the delivery price is split across the delivery’s goods in proportion to their net amounts, and each part is taxed like those goods.archivedtakes a method off checkout and keeps it for history.
The cart picks a method with selectShippingMethod; the quote shows each delivery’s offers.
Fulfillment Routing
/v1/stores/{store_id}/fulfillment-routing sdk.eshop.fulfillmentRouting.get() /v1/stores/{store_id}/fulfillment-routing sdk.eshop.fulfillmentRouting.update() const routing = await sdk.eshop.fulfillmentRouting.get({ store_id: storeId });
await sdk.eshop.fulfillmentRouting.update({
store_id: storeId,
expected_updated_at: routing.updated_at,
rules: [
{
id: crypto.randomUUID(),
key: "croatia-from-zagreb",
conditions: [{ type: "zones", zone_ids: [croatia.id] }],
target: { location_ids: [zagreb.id], assign: { type: "automatic", pick: { type: "in_list_order" }, split: { type: "never" } } },
status: { type: "active" },
},
],
otherwise: { location_ids: [sarajevo.id], assign: { type: "manual" } },
});
- A pickup is prepared at its pickup location, and a company location served from a store location
uses that location while it can deliver. Otherwise the first
activerule whose conditions all hold and whose target can deliver decides, andotherwisecatches the rest. - Rules match markets, sales channels, zones and shipping profiles; there are at most fifty. A location can deliver when it is active, has a shipping address and has a stock level for every tracked item of the delivery.
- A target assigns automatically (picking locations in list order or by most stock, and splitting a
delivery when needed or never) or leaves the choice to staff (
manual), holding the job until they confirm or move it. Units no location covers wait at the first location, heldout_of_stock. - Editing replaces the rules and
otherwisetogether and needs Store Admin.
Fulfillment Jobs
/v1/stores/{store_id}/fulfillment-jobs sdk.eshop.fulfillmentJob.find() /v1/stores/{store_id}/fulfillment-jobs/{id} sdk.eshop.fulfillmentJob.get() /v1/stores/{store_id}/fulfillment-jobs/{id}/decisions sdk.eshop.fulfillmentJob.decide() /v1/stores/{store_id}/fulfillment-jobs/{id}/items sdk.eshop.fulfillmentJob.findItems() /v1/stores/{store_id}/fulfillment-jobs/{id}/cash-collections sdk.eshop.fulfillmentJob.getPickupCash() /v1/stores/{store_id}/fulfillment-jobs/{id}/cash-collections sdk.eshop.fulfillmentJob.recordPickupCash() A job is one location’s share of one order delivery, or one rental replacement. Its method is a
delivery to an address from a location, with the source that chose the location (served_from, a
routing rule, otherwise or a staff move), or a pickup at a location. Its contact is read from the
order when the job is read, so a corrected contact shows at once.
- Its status is computed when read:
scheduledbefore its delivery window starts,on_holdwhile it has a hold,open,in_progressonce something left, thencompletedorcancelled. - A scheduled job reserves no stock; its location reserves when the window opens, and a shortfall
then holds the job
out_of_stock. - Holds stop goods leaving and are released one by one:
awaiting_payment(card and manual orders, until the order owes nothing),awaiting_location_choice,out_of_stock(lifts when stock arrives),manualwith a note, andhanded_backwith a note. itemsshows a packer each line’s product key, SKU, current blocks and main picture, without prices, so packing needs no catalog permission.
const job = await sdk.eshop.fulfillmentJob.get({ store_id: storeId, fulfillment_job_id: jobId });
await sdk.eshop.fulfillmentJob.decide({
store_id: storeId,
fulfillment_job_id: job.id,
expected_updated_at: job.updated_at,
action: { type: "move", to_store_location_id: zagreb.id },
});
Staff act on a job with one action and the expected_updated_at they read: move it to another
location routing names, split units that haven’t left into a new job there (with the id the app
picks), hand_back with a note, hold with a note, or release_hold naming the hold. Moving and
splitting need fulfillment everywhere and happen before anything has left; pickup jobs never move.
Staff limited to some locations work their jobs there and hand them back. A repeated action finds the
job changed and is refused.
A clerk with fulfillment reach at a pickup location records the cash of a cash-on-delivery pickup
with recordPickupCash, carrying the job’s and the payment’s versions; the pickup is collected only
once its cash is recorded. getPickupCash gives that clerk what to record without reading the order
or its payments: { payment_id, payment_updated_at, due }, or null when the job is not a pickup of
an order with an open cash-on-delivery payment, or the order owes nothing.
Fulfillments
/v1/stores/{store_id}/fulfillments sdk.eshop.fulfillment.create() /v1/stores/{store_id}/fulfillments sdk.eshop.fulfillment.find() /v1/stores/{store_id}/fulfillments/{id}/commands sdk.eshop.fulfillment.execute() /v1/stores/{store_id}/fulfillments/{id}/tracking sdk.eshop.fulfillment.updateTracking() /v1/stores/{store_id}/fulfillments/{id}/delivered sdk.eshop.fulfillment.markDelivered() const parcel = await sdk.eshop.fulfillment.create({
store_id: storeId,
id: crypto.randomUUID(),
fulfillment_job_id: job.id,
lines: [{ fulfillment_job_line_id: lineId, unit_spans: [{ first_unit: 0, quantity: 2 }], selected_units: [], lot_reference: null }],
});
await sdk.eshop.fulfillment.execute({
store_id: storeId,
fulfillment_id: parcel.id,
expected_updated_at: parcel.updated_at,
action: { type: "fulfill", timing: { type: "on_time" } },
notify_customer: true,
trackings: [{ carrier: "Post", number: "RR123456789DE", url: null }],
lot_references: [],
});
- A delivery is
preparing, thendispatched(with who sent it and whether it was on time), thendelivered, and carries up to 20trackings, typed when it leaves or later. A pickup ispreparing, thenready, thencollected. Either can becancelledbefore the goods leave; goods that left come back through a return. - Goods leaving at or after the end of the delivery window need a
latetiming with a reason. - The fulfillment’s id is picked by the app, so a retried create returns the same fulfillment. Each
line names a line of its job; a line the job doesn’t have is refused with
409 FULFILLMENT.OWNER_MISMATCHand nothing is saved. markDeliveredsetsdelivered_at, which can be corrected but never to before the dispatch.- A ready pickup sends the
order_ready_for_pickupemail, a dispatchorder_dispatched, and the first deliveredorder_delivered, to the order’s contact in the order’s language, unless the action’snotify_customerisfalse. A rental replacement has no order and sends none. - Webhooks:
fulfillment.created,fulfillment.ready,fulfillment.dispatched,fulfillment.delivered,fulfillment.collected,fulfillment.tracking_updatedandfulfillment.cancelled.
Customer Delivery Timeline
const timeline = await arky.eshop.order.findFulfillments({ order_id: order.id });
The storefront reads each job of its order with its method, status (scheduled, pending,
in_progress, completed or cancelled), delivery timing and shipments, each with its own status
and times, without staff details.
Returns and rentals have their own records on the E-shop page.