Saudi commerce · Payments reliability

Salla payment reconciliation: an order is not a ledger

A production design for reconciling Salla orders, payment transactions and refunds without treating a webhook or order status as complete accounting evidence.

TOPIC HUBE-commerce Engineering
Original conceptual illustration of an e-commerce order, payment transaction ledger, refund path and reconciliation checkpoint; not a real Salla interface.
An editorial interpretation of the topic, followed by a practical execution diagram.

A paid order and a payment transaction describe related facts, but they are not the same record. The order explains what the merchant intends to fulfil. A transaction records movement through a payment rail. One order can contain more than one payment method, a refund can be partial, a webhook can arrive twice, and a network timeout can leave an action with an unknown result. If an ERP or finance service reduces all of that to `order.is_paid = true`, it loses the evidence needed to explain differences later.

Salla's current documentation exposes three useful views. Order Details includes the order payment method, a `payment_methods` collection in the documented response and order amounts. List Transactions and Transaction Details expose transaction identifiers, order references, amount and currency, provider and method, status and available actions. Store webhooks include `order.payment.updated` and `order.refunded`. These views should converge, but each serves a different operational purpose.

This is a current-documentation explainer, not a claim that payment reconciliation launched today. The documentation was verified on 28 September 2026. It focuses on application architecture; the merchant's accounting treatment, settlement reports and tax obligations still need the relevant financial and regulatory evidence.

Model money as an append-only ledger

Keep the platform order as a commerce aggregate and create a separate local payment ledger. A durable transaction row should be keyed by merchant plus Salla transaction ID, not by order alone. Store the order reference, provider transaction reference when present, amount in minor units, currency, payment method, normalized status, source timestamp, last observed platform state and the raw evidence hash.

Do not overwrite financial history whenever a new webhook arrives. Append an observation or state transition, then derive the current projection. The same transaction may move from an intermediate state to a final state, while a refund should remain linked to the original charge. An append-only trail makes it possible to answer who observed the transition, from which payload, and which reconciliation run confirmed it.

Use integers for money after validating each currency's exponent. Avoid binary floating point and never compare formatted strings. Define explicit invariants per order: captured amount, refunded amount, net paid amount and order payable total must share a currency before arithmetic. Mixed or unexpected currencies belong in an exception queue, not an automatic conversion path.

Treat webhooks as signals, not final evidence

Subscribe to the smallest event set that repairs your projection: usually `order.payment.updated`, `order.refunded` and the order events already required by the integration. Verify Salla's signature over the raw request body, write the event to a durable inbox, commit, and acknowledge quickly. The public endpoint should not post to the ERP or issue a refund.

Build a deduplication key from the merchant, event type, stable order or transaction reference, and stable payload fields available in the documented model. Retain a payload hash. Even with that key, make every downstream transition idempotent because two different events can legitimately describe the same resulting state.

A worker should use the event as a reason to read current evidence. If a transaction ID is available, fetch Transaction Details. Otherwise query transactions for the order reference. Record the event time separately from the platform record's time and your observation time; those three clocks help diagnose reordering. A webhook saying payment changed does not, by itself, authorize fulfilment when the follow-up read is unavailable or contradictory.

Reconcile orders against transactions

For each changed order, compute a reconciliation snapshot from transaction rows. Salla's documented transaction response links a transaction to `reference_id`, `order_id` and `cart_id`, and includes amount, currency, payment method and status. The list endpoint documents filters including order ID, status, payment method, amount and last four digits. Use the order ID for deterministic repair; treat customer data and card fragments as sensitive lookup aids, not business keys.

Compare at least four dimensions. Identity asks whether every local transaction maps to the intended merchant and order. Currency asks whether all money being summed is comparable. State asks whether local normalization matches the current platform slug. Amount asks whether captures minus confirmed refunds equal the net payment projection. The outcome is not just `matched` or `failed`; use `matched`, `temporarily_incomplete`, `ambiguous`, `overpaid`, `underpaid`, `currency_mismatch` and `manual_review`.

Order Details remains valuable for the commerce side of the comparison, but do not couple reconciliation to the retired expanded response. Salla documents that the legacy expanded response ended on 1 September 2026 and the light format omits several nested resources. Read only documented payment and amount fields from the order endpoint, and use dedicated transaction endpoints for transaction truth.

Let payment webhooks wake the workflow; let transaction records, money invariants and reconciliation decide the durable state.
Let payment webhooks wake the workflow; let transaction records, money invariants and reconciliation decide the durable state. Open for a larger view

Make refunds a command with a durable outcome

Salla's Update Transaction endpoint supports `refund`, `void` and `reverse`, including partial refunds, under `transactions.read_write`. The documentation also warns that the store needs enough balance for refund amounts. This is a money-moving command, so a generic retry loop is unsafe.

Before calling it, create a refund command with a unique business key such as merchant, original transaction, return or case reference, amount and currency. Move it through `requested`, `dispatching`, `observed_succeeded`, `observed_failed` or `unknown`. If the request times out, do not issue a second refund immediately. Read Transaction Details and the order's current refund evidence, then reconcile. Only a proven absence should make a controlled retry eligible.

Validate the requested amount against the refundable balance calculated from confirmed ledger entries, not from a stale page. Require the currency to equal the original transaction currency. Serialize concurrent refund commands per transaction, and use database constraints so two workers cannot reserve the same refundable amount. Separate approval from execution when the merchant's controls require it.

Run bounded reconciliation continuously

Event-driven repair gives low latency; scheduled reconciliation provides completeness. Maintain a high-water mark per merchant and scan a bounded recent window of transactions plus every unresolved command. Overlap the window so late updates are seen again, and let idempotent upserts absorb duplicates. Persist pagination progress and respect rate-limit headers rather than letting one large store starve others.

Reconcile in layers. Run a fast loop for new or changed orders, a slower loop for open exceptions and a daily summary for settled business dates. Prioritize unknown refund outcomes and paid orders waiting for fulfilment over historical matches. Never auto-correct the ERP from an ambiguous row; open a case with the exact order, transaction, expected amount, observed amount, currency and source timestamps.

Useful service-level indicators include webhook-to-ledger delay, unmatched paid orders, transactions without orders, unknown refund age, duplicate events suppressed, amount mismatches by provider, rate-limit backoff time and the share of cases that resolve automatically. Alert on growing exception age, not only raw queue size.

Roll out with accounting-grade tests

Test duplicate and out-of-order webhooks, a successful payment followed by a delayed failure signal, split payment methods, partial refunds, two concurrent refund requests, timeout after Salla accepted the command, a missing transaction read, currency mismatch, token expiry and a reconciliation page replayed after a crash. Inject an intentional dropped event and prove the scheduled scan repairs it.

Start in shadow mode. Ingest events, build the ledger and compare it with the merchant's existing operational report without changing fulfilment or accounting. Explain every mismatch before enabling automation. Then allow low-risk state updates while keeping refunds behind approval, and finally enable tightly scoped automated commands with a kill switch.

The practical rule is simple: an order tells the business what to fulfil, a transaction tells the payment service what happened, and the reconciliation ledger records why your system believes both agree. Webhooks make that belief timely; repeated reads, invariants and durable command outcomes make it trustworthy.

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 projectMember Plus