Saudi commerce · Loyalty architecture

Zid loyalty integrations: build around the ledger, not the balance

A practical architecture for Zid loyalty integrations using transaction history, command idempotency, read-back confirmation and scheduled reconciliation.

TOPIC HUBE-commerce Engineering
Original conceptual illustration of loyalty points flowing from a Saudi online store through a transaction ledger into pending, available, redeemed and expired states; not a real platform screenshot.
An editorial interpretation of the topic, followed by a practical execution diagram.

A loyalty balance looks like one number, but a production integration has to preserve several different truths: points can be available, pending, pending for deduction, redeemed or expired; a manual adjustment can time out after the provider accepted it; and a rule change can alter future behavior without proving what happened to earlier transactions. A local table that simply overwrites `points_balance` hides those differences and eventually drifts.

Zid's current customer loyalty endpoint exposes `points_balance`, pending balances, available and used totals, plus embedded history. Its separate points-history endpoint returns dated entries with direction, type, reason, order references, expiry date and point status. That is the useful architectural boundary: treat Zid as the authority for the program and customer history, while your application keeps a disposable, read-optimized projection and an auditable command log.

This is a current-docs explainer, not a product launch. The cited documentation was checked on 27 September 2026. It documents read, configuration and mutation primitives, but it does not state exactly-once delivery or an idempotency header for point adjustments. The design below therefore treats ambiguous network outcomes as a reconciliation problem rather than assuming a retry is harmless.

Model state, not one balance

Do not map every response into a single `loyalty_points` column. At minimum, keep the customer and store identifiers, observed available balance, observed pending positive and negative balances, the provider observation time, and a sync status. Keep the provider transaction history separately or retain stable fingerprints of entries already processed. The local copy is for storefront reads, support search and analytics; it is not a competing ledger.

Available and pending values answer different questions. The checkout experience needs spendable points. A post-purchase screen may show pending earnings. Support needs the movement that explains a difference. Finance or growth teams may need totals over a reporting period. Never infer spendable value from a lifetime positive total, and never promise a customer that pending points can be redeemed.

A practical projection might look like this:

create table loyalty_customer_projection (
  store_id text not null,
  customer_id text not null,
  available_points numeric not null,
  pending_points numeric not null,
  pending_negative_points numeric not null,
  observed_at timestamptz not null,
  history_cursor_fingerprint text,
  sync_state text not null,
  primary key (store_id, customer_id)
);

The fingerprint is intentionally an application concept, not a field promised by Zid. Build it from the documented history attributes that are stable for your use case, store the full provider response for disputed adjustments, and verify the method against realistic data before relying on it for deduplication. If the API later exposes a stable transaction identifier, prefer that identifier.

Put a command log in front of every point adjustment

The adjust-customer-points endpoint accepts a customer ID, a `+` or `-` direction, a point amount and a reason. Its documented response includes the resulting movement metadata. The documentation does not show a caller-supplied idempotency key. Consequently, blindly retrying a timed-out POST can double-credit or double-debit the customer.

Create an internal command before the request. Give it a unique business key such as `return:{return_id}:loyalty-reversal`, record the customer, direction, points and a human-readable reason, and enforce uniqueness in your own database. Then send only commands that have not been dispatched. After a success, read the customer summary or history and attach the observed movement to the command.

planned -> sent -> confirmed
              \-> unknown -> reconciled-confirmed
                          \-> review-required

If the connection fails after sending, mark the result `unknown`. Do not immediately POST again. Query history for a matching customer, direction, amount, reason and time window. If the evidence is unambiguous, confirm the original command. If it is absent after an appropriate delay, retry through a controlled operator or a worker with an explicit attempt policy. If two real business events can legitimately have identical values, include a unique support or order reference in the reason where that is acceptable and preserve the provider response.

This pattern does not manufacture exactly-once behavior. It makes each uncertainty visible and recoverable. A unique local command prevents two application workers from intentionally issuing the same adjustment; read-back confirmation deals with the provider outcome.

Separate program configuration from customer redemption

Zid documents endpoints for reading program settings, creating a points redemption method, setting expiration and updating cashback rules. The redemption-method example configures a rule such as a fixed-rate gift certificate and returns rule and reward metadata. That is program configuration. It should not be interpreted, without further documentation, as a direct command that redeems a particular customer's points.

Store configuration changes as versioned administrative events. Read the current settings before a change, validate the merchant's intended rate and thresholds, apply the update, then read the settings again. Record who approved it and when. A typo in a fixed-rate rule can affect every future redemption, so this path deserves stronger authorization than a customer-facing balance read.

Expiration is also a policy change, not just an integer. The documented points-expiration endpoint accepts a number of days, while history entries can carry their own expiry dates and expired status. The documentation cited here does not promise that changing the setting rewrites old transactions retroactively. Preview the business impact, test in an appropriate environment, and verify actual history rather than assuming old entries moved.

A safe integration records the mutation intent, calls Zid, confirms the result from customer history, updates a read projection and reconciles drift on a schedule.
A safe integration records the mutation intent, calls Zid, confirms the result from customer history, updates a read projection and reconciles drift on a schedule. Open for a larger view

Reconcile because success responses are not the whole system

The official pages cited for this article expose reads and writes for loyalty, but this review did not verify a loyalty-specific webhook that can close every state transition. Build a scheduled reconciliation loop instead of inventing one. Re-read customers recently touched by a command, customers with pending balances, and a rotating sample of active accounts. Compare summary fields, history and local projection; repair the projection from provider data and raise a review when a mutation cannot be matched.

Use different frequencies for different risk. Reconcile an unknown point adjustment within minutes. Refresh a storefront projection after the customer signs in or reaches checkout. Audit inactive customers much less often. Add jitter and bounded concurrency so a large merchant does not create an API burst. If history pagination or rate limits are not explicit in the documentation you rely on, measure them safely and keep page size and concurrency configurable.

The local projection should be replaceable. A full rebuild must be possible from Zid reads plus your command audit trail. If a manual database edit is required to make balances match, record it as a repair event with before-and-after values; otherwise the next reconciliation may silently reverse it.

Scope access and observe the failure modes

The documentation uses different scopes for different operations: customer reads use `loyalty_program.read`, writes use `loyalty_program.read_write`, while some settings and analytics pages identify `third_loyalty_read`. Request only what each component needs. A storefront reader should not hold credentials able to adjust points, and an analytics job should not share the mutation worker's token. Handle 401, 403 and 422 separately: authentication, authorization and invalid business input are not retryable infrastructure failures.

Track more than request success rate. Useful signals include the number of commands in `unknown`, reconciliation mismatches, oldest unsynchronized customer, adjustment confirmations by age, repeated 422 responses, and changes to program settings. Zid's loyalty analytics endpoint exposes aggregate earned, redeemed and pending points plus customer and redemption counts for a date range. Use those totals as a business-level cross-check, not as a replacement for transaction-level investigation.

A reliable rollout starts read-only. First import settings and customer summaries, then compare the projection with support-visible values. Add history ingestion and scheduled reconciliation. Only after those paths are observable should you enable manual adjustments, initially for a small operator group with low limits and explicit review. Finally, automate eligible adjustments while retaining the command log and a kill switch.

The key rule is simple: Zid owns the loyalty truth; your system owns the intent, evidence and customer experience around it. When balances differ, engineers should be able to name the command, find the provider movement, explain its status and rebuild the local view. That is what turns a points feature into a dependable financial-like integration.

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 serviceBackend engineering & API integrationsRelevant projectLogistics at scale