# Connected NAZ platform — Partner API v1

The portal keeps its existing URLs. Arabic is the default; `naz_locale` and the account preference select English without changing SSO callback URLs. A language-only update merges with the profile. Personal, business and platform administration are explicit account contexts. `/business/new` authenticates identity and returns directly to the business application, then opens its pending workspace. An existing commercial registration never grants membership.

## Connections and identity

An organization requests `/api/v1/connections`, with an optional store, HTTPS origin, API path, exact callback URLs and requested capabilities. A NAZ administrator approves each capability. `sandbox` data stays out of the public catalog and buyer invoices. Disabled connections and suspended sellers/stores stop processing. Rotation produces a new server secret and returns the connection to pending approval. Secrets are encrypted and only returned at creation/rotation.

OIDC uses Authorization Code, PKCE S256, state, nonce, exact redirect allowlists and explicit consent. User access tokens are separate from server HMAC keys. New partner clients include the `naz-partner-api` audience. Existing client identifiers and grants are retained. A connection does not prove a hosted platform supports external identity: Salla, Zid and Shopify identity activation requires proof of actual platform support.

## Events and synchronization

POST raw UTF-8 JSON to `/api/v1/partner/events`. Headers: `X-Naz-Connection` and `X-Naz-Signature: t=UNIX_SECONDS,v1=HEX`. Compute HMAC-SHA256 over the decimal timestamp, a dot, and the exact raw body, with the connection secret. The timestamp must be within 300 seconds. Re-sign a retry with the current timestamp; keep event ID and body unchanged. Same ID/different payload is a conflict. Older/equal entity revisions are stale. The entity revision is monotonic per connection and entity type, even across deletion/restoration.

Envelope: `version:1`, unique `id`, `type`, `entityId`, integer `revision`, ISO `occurredAt`, `data`. Supported types are product/service/plan upserts and deletions, subscription/invoice upserts, order.status and refund.approved. Schemas and source endpoints are in [the source OpenAPI contract](partner-source.openapi.json). Invalid events are isolated with a reason. A correction uses a new event ID and higher revision. Reception alone does not mean application: inspect `status` and `reason`.

Publish events after the source transaction commits, through a durable outbox, targeting visibility within one minute. The NAZ worker also pulls `/changes` every five minutes, at most 100 events per page, and performs a resumable daily reconciliation scan. Cursor progress is persisted. Partial pages never imply deletion; publish explicit tombstones. Historical import produces no points. Retry transport failures with the same event, using bounded backoff. Source request idempotency must survive process restarts.

## Source ownership and invoices

Source prices, tax classifications, stock, billing periods, plan benefits, subscription contract versions and invoice documents remain authoritative. A catalog price update does not reprice or charge an existing subscription. Plans support month, year, one-time and custom `intervalDays`; do not label arbitrary day periods as monthly. Use Arabic/English source fields when present; merchant/customer names are never machine translated.

Invoices are unique by connection + external ID, not invoice reference. The encrypted original is immutable; amount/identity/document changes are rejected. Publish an original PDF/XML path with a SHA-256 digest. The authenticated document proxy checks buyer account permission, current session and digest. Personal invoices bind to a verified normalized phone and source customer ID. Business invoices bind only to an approved commercial registration; an employee contact phone is discarded for matching. Conflicting source-customer bindings stay unassigned. No second ownership review is introduced.

## Checkout, refunds and dues

The SAR cart has one buyer context and at most five stores. Each source quotes final tax, shipping and stock reservation before a single Nalpay payment link is created. Quotes expire in 1–15 minutes. On failure NAZ releases every source reservation by `nazOrderId`, including requests whose response was lost. Sources must support that idempotent release.

Goods quotes echo exact requested lines. Service upgrade quotes echo the subscription revision and authoritative difference/period/tax. Subscription checkout is separate from the goods cart. Payment return pages are informational. Signed notifications are durably received, then payment is fetched server-side and matched to currency, amount, link, checkout and live mode. A paid order waits for idempotent source execution without another charge. A final source rejection queues a documented refund; late payments are refunded rather than fulfilled. Nalpay limits are respected without adding fees. No new sales commission is charged.

The ledger separates collection, pending seller liability, available liability, refund/withdrawal holds, receivables and completed transfers. Delivery, pickup or service activation releases only the remaining unrefunded, unreserved amount. Disputes block withdrawal. Seller bank verification and an independent reviewer are required. A bank change invalidates the pending transfer. Canceling a transfer first offsets seller debt. Default threshold is 250,000 halalas. No automated Nalpay payout capability is assumed; approval records an independently executed, verified bank transfer.

Partial refunds allocate cash/points cumulatively against the original allocation. Point restoration, earned-point reversal, refunds and journals are idempotent. Existing point funding, risk, issuer fees and VAT policies stay separate from seller sales invoices. An unmatched external provider refund freezes the checkout and seller withdrawals for finance reconciliation; the UI reports this exception rather than silently allocating it to a seller. This exceptional reconciliation requires case-specific finance evidence before any manual corrective release.

## Messaging

Infrastructure providers are separate from merchant connections. Taqnyat uses Bearer POST `/v1/messages`, with normalized international recipients without `+`. Provider acceptance is distinct from delivery confirmation; no delivery callback authenticity is assumed. Store message references and safe outcomes, never bodies/OTP codes/secrets. Primary and backup routes are independent for OTP, operational and marketing traffic. Only explicit rejection can fall back; unknown transport outcomes do not resend automatically.

Provider verification sends a random code to the acting administrator's own verified phone. Successful code verification enables OTP routing and disables the temporary owner code. Campaigns are platform-admin-only and separately permissioned. They include explicit consent, signed opt-out links, suppression, scheduling, recipient limits and conservative configured cost reservations. Configure the maximum per-recipient cost to include all SMS segments and provider charges; use provider-side spend limits as the final monetary ceiling. OTP is handled synchronously and separately from the bounded campaign queue.

## Pilot connector status and activation

`sdk/typescript` and `sdk/php` provide raw-body signing and event publishing. `connectors/php/SourceGateway.php` provides signed dispatch, durable monotonic records, an outbox and resumable reads. `schema.sql` is additive and must run with a source-specific account, never the NAZ production database account. Keep connector tables/keys outside public file serving.

`MarketingReadModel` maps actual `packages`, pricing periods, limits and features. Subscription export requires a verified workspace-to-approved-CR binding and the original purchased period/contract benefits; current catalog prices cannot reconstruct these. `KabayanReadModel` calls the existing StorefrontService and GoldPricingService, and accepts frozen issued-invoice snapshots. Complete product variants and explicit source deletions through the same outbox. Neither adapter advertises unimplemented financial capabilities.

Production activation still requires real source ownership/binding, source price/hold/invoice/commit/refund handlers and source acceptance tests. The inspected legacy marketing checkout updates user-level subscription fields, while workspace subscriptions are stored separately; it must not be reused for business upgrades. Original tax invoice generation and authoritative upgrade credits were not proven in that legacy path. Real Taqnyat, Nalpay and wallet credentials are also required. Do not call these pilots production-ready until these source/provider tests pass.


---

## Existing modules and permissions reference
# NAZ developer reference — v1

Issuer: https://sso.naz.sa/identity/realms/naz. Use OIDC Authorization Code + PKCE S256, state and nonce. Every approved store receives its own confidential client. Exact HTTPS callbacks must match its reviewed domain. Never put client secrets in a browser/mobile app. Stable identity is the verified `sub`; never merge identities by name or unverified phone.

The portal provisions clients for custom/WooCommerce integrations after separate SSO approval. Shopify external IdP requires verified Plus capability. Merchant OAuth for Salla/Zid does not prove shopper SSO support.

`GET /api/v1/partner/profile` accepts a user Bearer access token. Issuer, RS256 signature, azp, integration/store/organization status and active NAZ consent are checked. It returns only granted profile/phone/email/address fields. Customers grant or withdraw consent in NAZ. This is the supported token API; browser administration and financial routes use secure httpOnly sessions plus same-origin and X-CSRF-Token from `/auth/me`.

OpenAPI is at `/api/openapi.json`, interactive reference at `/api/docs`. Monetary amounts are integer SAR halalas; never floats. POST financial operations need a stable Idempotency-Key. Reusing a key with different input returns409. Authorize points for15minutes, then capture or void. Central PostgreSQL transactions and row locks prevent overspending; FIFO lots, immutable entries and balanced journals preserve history.

A 500000-halalas eligible net sale paid575000 cash earns50000points, costs10000 service fee plus1500VAT, and reserves5000halalas of reward value. Redemption5000halalas consumes50000points. New earning uses only the cash tender share of eligible pre-VAT value. Points do not expire and cannot be cashed out or transferred between individuals. Return amounts are cumulative. A return after spending does not reverse the receiving merchant payable; consumed rewards are borne by the issuer, with tax-reviewed fee credit proposals.

POST `/api/v1/sync-batches` accepts `{integrationId,records:[{sourceId,type,data}]}` (1000 records,2MiB maximum). Validation produces a content digest and row errors. Admin approval and a separate commit require the same digest, rerun identity/consent validation, and publish atomically. No historical points import exists. Products require name, integer priceMinor and currency SAR; offers require ordered ISO timestamps. Customer links require an existing personId and an active customer_sync consentId for that integration. Financial orders are rejected by generic import; they require a verified provider event matching every amount and identity field.

Nalpay keys and webhook secrets are encrypted separately. The server creates hosted subscriptions through the transactional outbox with stable idempotency keys. Consent/payment card collection happens at Nalpay. A monthly cycle is30days; annual365days. `/hooks/nalpay` verifies the raw t.body HMAC, timestamp, delivery/event IDs and mode, then persists before acknowledging. The worker fetches the provider resource and validates amount/currency/reference; success redirects are not proof. Failed work has durable exponential retries and a review state. Refund events require finance/tax reconciliation. Merchant payout support is an independent provider contract, not an assumed Nalpay capability.

Live operation is disabled until real messaging, payment capabilities and regulatory/tax/privacy/accounting evidence are approved. The temporary setup login applies to the designated administrator only; financial calls reject bootstrap sessions. Public registration remains closed before privacy approval. Provider adapters without credentials remain unverified, including pagination/deletion/refund mapping acceptance. Run connector tests against independent sandbox databases/keys/networks, never production balances.

See `developers.ar.md` for the full route table and data contracts, `operations.md` for deployment/restore, and `readiness.md` for the acceptance matrix. No card numbers, tokens, OTPs or raw webhook bodies belong in application logs.

## Active account

A login with multiple available contexts lands at /accounts. GET /api/v1/accounts lists personal, delegated businesses and the admin workspace. POST /api/v1/accounts/select accepts accountId (personal, admin, org:UUID), rotates CSRF and returns its landing URL. Platform admin capability is effective only after explicitly choosing the admin workspace. Business data is scoped to the selected organization, with live membership checks. X-Naz-Account prevents stale tabs from operating under a different account; mismatch returns409 account_changed, while an unselected session returns409 account_selection_required. The UI reloads into a clean context after selection. Distinct phone identities are never silently linked.

## Membership wallets and stores

See wallets.md for issuer setup and limitations. GET /api/v1/wallet/membership exposes the membership identifier and provider readiness only. POST /api/v1/wallet/google returns an RS256 signed save link; POST /api/v1/wallet/apple returns a base64 encoded signed pkpass. All require a personal account and authenticated session; issuance also requires CSRF and approved issuer credentials. Barcodes never authorize payments, redemption or sign-in. Current balances stay in the authenticated NAZ portal.

Within a business context, POST /api/v1/stores must target its active organizationId; another tenant is rejected. Creating a separate legal entity requires personal/admin context. UI currency uses the supplied official Saudi Riyal SVG; API currency remains SAR with integer halalas.

## Membership tiers and platform team

Membership status includes tier, next, tiers, qualifiedPoints, remainingPoints and progressBps. Qualification sums posted earned and purchase_refunded entries; redemption never reduces the tier. The default gold threshold is1000. PATCH /api/v1/settings/membership accepts a validated ascending tiers array with stable unique IDs, names, minimumPoints, theme and benefits; the first threshold is0, with2–8tiers. Device detection only selects the displayed wallet and never grants authorization.

GET/POST /api/v1/team and PATCH /api/v1/team/UUID are owner-only in the admin context. Create accepts name, phone and role(admin/viewer); update accepts name, role and enabled. A staged phone invitation is activated only after verified phone sign-in and identity binding. Changing access revokes active sessions. auth/me exposes platformRole, canManageTeam and readOnly. AuthGuard enforces viewer write denial on the server. Team delegation never changes the platform owner or grants team members the fixed bootstrap code. See membership-and-team.md for exact visibility scope.


## Platform team permissions

See docs/team-permissions.md for request contracts and permission catalogue. auth/me returns the resolved roleName and permissions. Every administrative action is checked server-side; dashboard data is projected by page access. Grant changes revoke sessions. Platform roles are separate from business memberships.


Team activity: GET /api/v1/team/activity requires team.view and team.activity in the active admin account. Supports member/type/date filters in Asia/Riyadh and stable cursor pagination. See team-permissions.md for scope, safe changes and legacy limitations.

## Team activity exports
POST /api/v1/team/activity/export accepts format (xlsx or pdf), optional memberId/type/action/from/to. Requires team.view + team.activity + team.export in an active admin account. Inclusive Asia/Riyadh calendar dates; consistent full snapshot, never silently truncated. Limits: 5000 XLSX / 200 PDF events. Export is audited and session/grants are rechecked before delivery. See OpenAPI for binary response headers and errors.

The owner confirmed marketing plan prices are tax-inclusive. The mid-term upgrade policy is still unspecified; no upgrade is charged before it is defined and tested in the source.
