Skip to main content

Shipping and delivery

Shipping in ARC means booking a courier, called a delivery partner, to carry goods out to a customer or to collect a customer return. A booking creates a Shipment. You then follow it to delivery and record what you paid the courier.

A shipment has a direction: OUTBOUND for an order's delivery, RETURN for a pickup from the customer. Both appear in the same list and use the same operations.

Connecting couriers, changing delivery settings, booking, cancelling and updating a shipment need the delivery partners feature on your plan. Plain reads of shipments, settings and configuration do not. Each operation also needs the permission shown in its section. For the exact field list of every type, see the API reference. The examples use <...> for values you supply.

1. Plan the integration​

JobOperationsPermission
Connect a courierdeliveryPartnerProviders, deliveryPartnerConfigs, upsertDeliveryPartnerConfig, testDeliveryPartnerConnection, registerDeliveryPartnerWebhook, setDeliveryPartnerEnabled, reorderDeliveryPartners, deleteDeliveryPartnerConfigsettings:integration:read, settings:integration:update, settings:integration:delete
Configure deliverydeliverySettings, deliveryPackingSettings, deliveryBoxPresets, deliveryCustomerDisplay, deliverySeenServices and their update... mutationssettings:sales:read, settings:sales:update
Product and category defaultsproductDeliveryFlags, categoryDeliveryDefaults, productVariantsMissingShippingData and the update... mutationsproducts:product:read, products:product:update, categories:category:read, categories:category:update
Quote and bookstaffDeliveryOptions, bookableDeliveryPartners, shipmentRates, packingRatePreview, bookShipmentForShippedGoods, bookReturnPickup, cancelShipmentsales:sales_order:create, sales:sales_order:ship, sales:shipment:create, sales:shipment:cancel
Follow and close outshipments, shipment, trackShipment, refreshShipmentTracking, recordShipmentStatus, markShipmentCourierPaid and the rest of section 6sales:shipment:list, sales:shipment:read, sales:shipment:update, sales:sales_order:ship

A user who is limited to their own customers sees only the shipments whose order or return they can open. An out-of-scope shipment is returned as not found. The cost columns on shipments are filled only for callers with sales:sales_order:ship.

2. Connect a courier​

Do this once per courier, in this order. A courier can carry shipments only after it has passed a connection test and been switched on.

List what you can connect​

Requires settings:integration:read.

query Providers {
deliveryPartnerProviders {
key
displayName
kind
capabilities
supportedCountries
supportedCurrencies
webhookAuth
}
}

Read the credential and settings fields a provider declares, and build your form from them. deliveryPartnerConfigs lists the couriers already set up. It never returns a secret. It lists which secret keys are stored in configuredSecretKeys.

Save, test, register, enable​

  1. upsertDeliveryPartnerConfig creates or updates a configuration. Requires settings:integration:update. Secret fields are write-only and stored encrypted, and an empty value keeps the stored one. Setting up a provider that is already set up is refused. Changing credentials takes an enabled partner offline until you verify it again.
mutation SaveCourier($input: UpsertDeliveryPartnerConfigInput!) {
upsertDeliveryPartnerConfig(input: $input) {
id
isEnabled
verification {
keysValid
needsReverification
webhookReachable
}
}
}
{
"input": {
"providerKey": "<provider key from deliveryPartnerProviders>",
"environment": "<environment>",
"displayName": "<display name>",
"credentials": [{ "key": "<credential field>", "value": "<secret>" }],
"settings": [{ "key": "<settings field>", "value": "<value>" }],
"useForCheckout": true
}
}
  1. testDeliveryPartnerConnection(id) makes a real call to the courier, records the result and returns the courier's exact rejection when it fails. A partner must pass this before it can be enabled. A manual courier is verified when you save it.
  2. registerDeliveryPartnerWebhook(id) registers your organization's webhook URL through the courier's own API. A provider without such an API is refused, with a note that its webhook is set up in the courier's own dashboard.
  3. setDeliveryPartnerEnabled(id, enabled: true) turns the courier on. It needs a verified connection and, for a courier that can only report status by webhook, a webhook that has already arrived.

reorderDeliveryPartners(ids) sets the order, top first. The order drives the partner list, the automatic partner-order rule and every tie-break. Send at most 200 ids, with none repeated. deleteDeliveryPartnerConfig(id) (requires settings:integration:delete) switches the partner off and stops its webhook URL for good. You can set the same provider up again later.

3. Configure delivery​

These settings shape what customers are offered and how boxes are packed. They take effect at the next quote.

  • deliverySettings and updateDeliverySettings hold how the delivery fee is charged (feeMode: FLAT or LIVE_QUOTE), the flat fee, the free-delivery thresholds, the default pickup warehouse and the customer delivery emails.
  • deliveryPackingSettings, updateDeliveryPackingSettings and deliveryBoxPresets hold the boxes used for packing, in centimetres and kilograms.
  • deliveryCustomerDisplay and updateDeliveryCustomerDisplay hold how options are shown to customers. Omitted fields keep their saved value. deliveryCustomerDisplayPreview shows, without saving, what customers and staff would see for one address. It calls the couriers.
  • deliverySeenServices lists the courier services each partner has returned, with when each was last seen and whether customers are offered it.

Reads need settings:sales:read. Changes need settings:sales:update.

Boxes are packed from each variant's own weight and size. Find the variants that cannot be packed yet:

query MissingData($take: Int) {
productVariantsMissingShippingDataCount
productVariantsMissingShippingData(take: $take) {
total
skip
take
}
}

Per product, updateProductDeliveryFlags sets whether it ships in its own packaging and whether it is fragile. A flag you leave out keeps its value, and null clears it. Per category, updateCategoryDeliveryDefaults replaces the category's defaults and keeps its other metadata. categoryDeliveryDefaults returns both the category's own defaults and what applies to the items in it.

4. Quote and book a shipment​

Before the order exists​

staffDeliveryOptions returns live options and prices for a draft sales order. Requires sales:sales_order:create. The customer must be in the caller's own book. Pass the chosen option's ID to createSalesOrder as its delivery option. Without the delivery partners feature it answers that none are available, and the manual shipping amount stands. When an order packs above the store's freight threshold, no courier is asked and the answer carries the code DELIVERY_FREIGHT_QUOTE. You may place the order without an option, and staff quote the delivery charge before invoicing.

Price a shipment​

shipmentRates prices a shipment with every enabled partner, at your cost. Requires sales:sales_order:ship. Give exactly one of salesOrderId or productReturnId. A return pickup is offered only by partners that handle returns, and lines apply to a sales order, not a return.

query Rates($input: ShipmentRatesInput!) {
shipmentRates(input: $input) {
options {
deliveryPartnerConfigId
partnerDisplayName
serviceLevelCode
serviceLevelName
amount
currency
providerQuoteRef
expiresAt
}
unavailable { partnerDisplayName reason retryable }
suggestedParcels { name lengthCm widthCm heightCm weightKg }
}
}

Partners that could not quote are listed in unavailable with the reason, so one failing courier never hides the others. packingRatePreview packs given lines and prices both packings with every enabled partner. It uses the same rule as checkout and stores nothing. bookableDeliveryPartners lists the enabled partners staff can book with, without secrets, for a ship dialog or pickup picker.

Book​

Both booking mutations need sales:shipment:create.

  • bookShipmentForShippedGoods(input) books a courier for goods that have already shipped from a sales order. It never issues stock. When every shipped item is already covered by a shipment, it is refused and asks you to ship the remaining items first.
  • bookReturnPickup(delivery, productReturnId) books a courier to collect a customer return, using a partner set up for returns. The booking is all-or-nothing and shows the courier's own error if it fails.
mutation Book($input: BookShipmentForShippedGoodsInput!) {
bookShipmentForShippedGoods(input: $input) {
id
status
carrierName
trackingNumber
errorMessage
}
}
{
"input": {
"salesOrderId": "<sales-order-id>",
"delivery": {
"deliveryPartnerConfigId": "<partner config id from the rate option>",
"serviceLevelCode": "<service level code from the rate option>",
"providerQuoteRef": "<providerQuoteRef from the rate option>",
"quotedAmount": "<amount from the rate option>",
"quotedCurrency": "<currency from the rate option>"
}
}
}

Copy the quote fields from the rate option you chose so the booking matches what you were shown. shipmentCustomerPhone returns the number a booking falls back to when you give no recipient phone: the shipping address phone, then the order's mobile, then the customer's contact. Give exactly one of salesOrderId or productReturnId.

Cancel​

cancelShipment(id, reason) cancels with the courier. Requires sales:shipment:cancel. Stock is not returned: a return does that. A shipment the courier already holds (picked up, in transit or out for delivery), or one that has finished, cannot be cancelled. That is checked before the courier is contacted.

5. Track a shipment​

List and open shipments with shipments, shipment, salesOrderShipments and productReturnShipments. Requires sales:shipment:list for the tenant-wide list and sales:shipment:read for the rest. shipments is newest first and takes skip and take (25 by default, at most 100) and a filter by status group, direction, search, created window and courier payment.

Shipment statuses are DRAFT, SUBMITTED, BOOKING, BOOKED, BOOKING_FAILED, BOOKING_ORPHANED, AWAITING_PICKUP, PICKED_UP, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED, FAILED_ATTEMPT, EXCEPTION, RETURNED_TO_SENDER, LOST and CANCELLED. The list tabs group them as PENDING, BOOKED, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED, PROBLEM and CANCELLED. shipmentStatusGroupCounts and shipmentsSummary return the counts under a filter.

query Track($shipmentId: ID!) {
trackShipment(shipmentId: $shipmentId) {
checkedAt
carrierError
shipment {
id
status
trackingNumber
trackingUrl
}
}
}
  • trackShipment returns the shipment, its progress steps and when the courier was last asked. Opening it asks the courier, and the answer is reused for 60 seconds. refresh: true asks again unless the last ask was under 10 seconds ago. A courier failure sets carrierError to the courier's own words and still returns the saved timeline.
  • refreshShipmentTracking(id) asks for the latest status now and updates the shipment. Requires sales:shipment:update.
  • shipmentLiveLocation(shipmentId) returns the courier's live position, cached for 15 seconds per shipment. It is null unless the courier supports live location and is carrying the shipment (picked up or out for delivery).
  • shipmentLabelUrl, shipmentProofOfDeliveryUrl, shipmentPackingListUrl, shipmentStickerUrl and shipmentsStickersUrl (up to 100 ids) return short-lived signed download links. Fetch the link when you need the file, not ahead of time.

For a custom courier that reports no status itself, record it by hand with recordShipmentStatus(id, input). Requires sales:shipment:update. A courier that reports its own status is refused: use refreshShipmentTracking. Only the statuses staff may set are accepted, and the move must be allowed from the current status. Marking an order's outbound shipment DELIVERED also needs the sales order deliver permission, because it can mark the order delivered.

A booking that needs checking​

A shipment in BOOKING_ORPHANED is a booking whose outcome is unknown. Check with the courier, then call resolveOrphanedShipment(id, input). Requires sales:shipment:update. CONFIRM_BOOKED records that the courier holds the booking, optionally with its tracking number. MARK_CANCELLED records that you cancelled it there. It never calls the courier, and a shipment in any other status is refused.

6. Record what you paid the courier​

These operations record payment only. No money moves. All need sales:sales_order:ship.

  • markShipmentCourierPaid(shipmentId, input) records date (YYYY-MM-DD), method and reference. It is refused for a cancelled shipment or a failed booking. Marking again overwrites the record, and every change is audited.
  • markShipmentsCourierPaid(input) records one payment on 1 to 500 shipments. It is all or nothing: it is refused when any shipment is not found, is cancelled or failed, or is already marked paid. It returns the courier charges settled per currency.
  • clearShipmentCourierPayment(shipmentId) removes a payment recorded by mistake.

7. Common errors​

What you seeWhyWhat to do
A booking, cancel or settings change is refused for your planThe delivery partners feature is not on your planEnable the feature, or use the manual shipping amount
Enabling a partner is refusedThe connection is not verified, or a webhook-only courier has not sent a webhook yetRun testDeliveryPartnerConnection, then registerDeliveryPartnerWebhook, then enable
Saving a configuration is refusedThe provider is already set upUpdate the existing configuration by its id
A courier is missing from the rate optionsIt could not quoteRead its entry in unavailable. retryable says whether to try again
bookShipmentForShippedGoods asks you to ship the remaining itemsEvery shipped item already has a shipmentShip the remaining items first
cancelShipment is refusedThe courier already holds the parcel, or the shipment has finishedWait for delivery, then arrange a return
recordShipmentStatus is refusedThe courier reports its own status, or the move is not allowed from the current statusUse refreshShipmentTracking, or pick an allowed status
trackShipment returns carrierErrorThe courier call failedShow the saved timeline and the courier's message, and retry later
markShipmentsCourierPaid is refusedOne shipment is missing, cancelled, failed or already paidRemove it from the list and retry

Booking and rating failures carry the courier's own words, so show them to the user instead of replacing them with a generic message.