Saudi commerce · Shipping reliability

Salla shipments: build a state machine that can recover

A production design for keeping Salla, a shipping carrier and an ERP consistent through duplicate webhooks, delayed events, unknown outcomes, cancellations and returns.

TOPIC HUBE-commerce Engineering
Original conceptual illustration of a parcel moving through delivery, retry, reconciliation and return paths; not a real Salla interface or event image.
An editorial interpretation of the topic, followed by a practical execution diagram.

A shipment exists in at least three systems: Salla, the carrier and usually an ERP or fulfilment service. Each system can be correct locally while the merchant still sees a contradiction. The carrier may have accepted a label when the integration timed out, Salla may retry a webhook, an ERP may receive a later status before an earlier one, or a cancellation may be requested after the parcel has already left the warehouse. Treating the latest payload as the truth turns those normal distributed-system conditions into duplicate labels, regressed statuses and unexplained customer messages.

Salla's current shipment API exposes creation, listing, detail, update, cancellation, return and tracking operations. Its store events include `shipment.creating`, `shipment.created`, `shipment.updated` and `shipment.cancelled`. The webhook documentation also says an unsuccessful delivery can be retried three times at roughly five-minute intervals. Delivery is therefore at least once from the receiver's perspective; application code must expect repetition.

This is a current-documentation explainer, not an announcement that Salla launched a new shipping feature today. The API documents were verified on 29 September 2026. The state machine, command ledger and reconciliation design below are architectural recommendations built around those documented operations.

Separate observations, state and commands

Do not make a webhook handler both decide the shipment state and call the carrier. Persist three different records. An observation stores exactly what Salla or the carrier reported, with source, source timestamp, receipt time and a hash of the raw payload. A shipment projection stores the latest accepted business state. A command records an intended external effect such as create label, cancel shipment or request return.

This separation preserves evidence. A duplicated `shipment.updated` observation can be acknowledged without applying a second transition. An out-of-order status remains available for audit without rolling `delivered` back to `in_transit`. A timed-out carrier call stays `outcome_unknown` until reconciliation instead of being mislabeled failed.

Use stable identities. A local shipment key should include the merchant and Salla shipment ID once known. Before Salla returns an ID, use a unique business operation key such as merchant, order, shipment type and fulfilment attempt. Store Salla's order reference, carrier tracking number, external IDs and return relationship as references, not interchangeable primary keys. One order can produce more than one shipment and a return is a distinct lifecycle.

Normalize without erasing source detail

Salla's shipment detail model currently documents states including `created`, `in_progress`, `in_transit`, `received_at_final_hub`, `to_be_reattempted`, `reattempted`, `unable_to_deliver`, `delivering`, `delivered`, `partially_delivered`, `cancelled`, `lost`, `damaged`, `return_to_origin` and `return_in_progress`. Carrier vocabularies will differ. Map both sides to a compact internal model, but keep the original status beside it.

A useful internal model might group label preparation, handoff, movement, delivery exception, terminal delivery, cancellation and return. Grouping supports product logic; source detail supports support and audit. Do not collapse `lost`, `damaged` and `unable_to_deliver` into a generic failure that hides the required next action. The customer message, inventory treatment and carrier claim process differ.

Version the mapping table. When a carrier adds a state or Salla extends its enum, old observations must still be interpreted under the mapping that received them. Unknown values should enter a visible quarantine queue rather than silently defaulting to `in_transit`.

Enforce a transition policy

The projection should change only through an explicit transition function. Salla's shipping app cycle states that after a shipment reaches `shipped`, `delivering` or `delivered`, it cannot be moved back to `created` or `in_progress`. Your local policy should be at least as strict and should define additional business invariants.

apply(observation, current_state, mapping_version)
  -> accepted(next_state, reason)
  -> ignored_duplicate(reason)
  -> quarantined(reason)
  -> conflict(needs_review)

Use source sequence or source timestamps only when their semantics are documented and trustworthy. Receipt time is not event time. If no authoritative ordering value exists, compare transitions against invariants: delivered is terminal for the outbound leg, cancellation after carrier handoff requires confirmation, and return progress belongs to a linked return leg rather than rewinding the outbound shipment.

Record every decision with the observation ID, previous state, proposed state, rule version and reason. That history lets support explain why a late event was ignored and lets engineers replay the projection when a mapping bug is fixed.

A recoverable shipment flow separates observations, accepted transitions and carrier commands, then reconciles every ambiguous outcome.
A recoverable shipment flow separates observations, accepted transitions and carrier commands, then reconciles every ambiguous outcome. Open for a larger view

Make every carrier command idempotent

Shipment creation is the most dangerous ambiguous operation. Salla's Create Shipment endpoint accepts order and courier data plus external references; the current document also requires National Address details in `ship_to`. Before calling a carrier, insert a command row with a unique operation key. Send that same key as the carrier's idempotency or customer reference when supported.

A retry must resume the same command, not create a new business intent. If the HTTP request times out, mark the command `outcome_unknown`. Query the carrier by the operation key or order reference. Only create again after evidence shows no shipment exists. Save the returned label, tracking number and provider receipt transactionally with the successful command state.

Apply the same protocol to cancel and return operations. A `200` from a local queue only proves acceptance of work, not carrier completion. Conversely, a timeout does not prove rejection. Model `requested`, `dispatched`, `confirmed`, `rejected` and `outcome_unknown` separately so the interface can tell the merchant what is known.

Treat webhooks as hints and reconcile

Verify `X-Salla-Signature` against the unmodified request body with a timing-safe comparison, persist the observation, then respond quickly. Heavy carrier or ERP work belongs on a durable queue. The documented retry behavior is useful recovery transport, but it is not an exactly-once contract and should not be the only recovery mechanism.

Create a scheduled reconciliation loop. The List Shipments endpoint supports filters such as order, courier, status, shipment type, date range and pagination. Use bounded windows with overlap, then fetch detail or tracking history for suspicious rows. Compare Salla, the carrier and the local projection by identities, current states, tracking references and terminal timestamps.

Do not let reconciliation blindly overwrite. It emits observations through the same transition function as webhooks. This keeps one set of invariants and produces an audit trail. Classify drift: missing local shipment, missing provider receipt, state disagreement, identity mismatch, stale movement or terminal-state conflict. Route each class to an automatic repair or a human queue.

Design cancellation and return as sagas

Cancellation is not a boolean field. The merchant requests it, the integration validates current evidence, the carrier accepts or rejects it, Salla is updated, and inventory or customer communication follows. Current Salla guidance notes that a shipping company can reject cancellation after dispatch or delivery. Preserve that rejection as a business result, not a generic integration error.

Returns need their own shipment leg. Salla's shipping flow documents `shipment.creating` with `type: return`, and the Return Shipment endpoint acts on a specific shipment ID. Link the return to the outbound shipment, but give it its own label, tracking, commands, projection and terminal state. Returning a parcel does not erase that the outbound delivery occurred.

Use compensating actions where atomicity is impossible. If the carrier cancels but updating Salla fails, reconciliation should retry the Salla projection update. If Salla records cancellation but the carrier rejects it, surface a conflict and prevent inventory from being released automatically. Every step needs an owner, deadline and recovery action.

Protect schema and address boundaries

The current Create Shipment documentation says National Address fields are mandatory in `ship_to`, and deprecates older country and city identifiers in favor of the newer nested fields. Validate the complete address before creating the external command. A missing postal code or short address should fail before a carrier label is purchased, with a repairable merchant-facing message.

Treat API constraints as contract tests. Salla's current Update Shipment Details document requires `status` and limits `tracking_link` and `pdf_label` to 300 characters. Keep fixtures for maximum lengths, optional values, COD amounts, multiple packages, outbound versus return type and each supported terminal state. Alert on unknown fields only when they affect correctness; preserve forward-compatible payloads in raw evidence.

Redact labels, phone numbers, addresses and customer data from logs. Observability needs correlation IDs, merchant-scoped shipment IDs, command IDs, state transitions and error classes, not full payload dumps. Encrypt retained raw evidence and apply a deletion policy that matches operational and legal needs.

Measure recovery, not just webhook success

A green webhook response rate can hide broken fulfilment. Track time from merchant request to confirmed label, commands in `outcome_unknown`, duplicate observations suppressed, quarantined transitions, shipments with no movement, disagreements by carrier, reconciliation repair time, cancellation conflicts, returns without a linked outbound leg and customer notifications sent from unconfirmed state.

Test by cutting the connection after the carrier commits but before your worker stores the response. Replay the same webhook several times. Deliver statuses out of order. Delay a cancellation until the parcel is in transit. Fail the Salla update after a confirmed carrier return. The expected result is not that every call succeeds; it is that one shipment identity and one explainable state eventually emerge.

The practical rule is simple: events report observations, a versioned state machine accepts transitions, command records own side effects, and reconciliation resolves uncertainty. That structure costs more than assigning the last payload to a column, but it prevents the costly outcome in commerce operations: a parcel moving in the real world while every system tells a different story.

Official references

These references document the tools discussed. Examples and design decisions are illustrative and should be adapted to the project and its versions.

Prepared by: Noor Yasser

FROM DECISION TO DELIVERY

Working through a similar engineering challenge?

I help teams turn architecture decisions into a clear scope and dependable, reviewable implementation.

Book a 30-minute callRelated serviceSalla & Zid apps and merchant toolsRelevant projectLogistics at scale