Files
zarinpal-analytics-backend/docs/API-SPEC.md

23 KiB
Raw Blame History

Merchant Analytics API Capability Spec

  • Status: Draft for review — implementation not yet authorized
  • Capability: Merchant API
  • Depends on: Approved Identity, Data Foundation, and Analytics Engine specifications
  • Interface source of truth: docs/openapi.yaml
  • Last updated: 2026-08-20

Objective

Expose the approved merchant identity and analytics capabilities as a tenant-safe REST JSON API under /api/v1. The API starts and observes analytics runs, streams partial results, lists merchant-owned evidence, and exports the same authorized detail view as CSV. OpenAPI is the normative interface contract and is intended to generate frontend types after approval.

Actors and trust boundaries

  • Unauthenticated browser: may call login and explicitly public health/static resources only.
  • Authenticated merchant user: may access one merchant derived from the current server-side session.
  • Merchant dashboard: same-origin browser client using the opaque session cookie and CSRF protection for mutations.
  • Analytics worker and Redis: private implementation boundaries; neither is exposed through this API.
  • Cross-merchant peer data: server-only calculation input. The API returns only approved anonymized aggregates or a suppressed state.

Every request, query parameter, cursor, run id, event id, and export request is untrusted. Possession of a resource id never grants access. The backend resolves the merchant from __Host-zpa_session for every protected request and checks resource ownership inside that boundary.

Scope

In scope for MVP

  • Login, current-session inspection, and logout
  • Approved reporting-period discovery with exact ISO boundaries and Persian/Jalali labels
  • Idempotent analytics-run start, snapshot retrieval, section retry, and authenticated SSE
  • Typed results for overview, payment_performance, fees, and peer_comparison
  • Cursor-paginated, read-only merchant-owned session details for unverified-paid, failed, and retried views
  • Synchronous CSV export of the currently authorized detail view and filters
  • RFC 9457 Problem Details, authorization, throttling, response-size limits, audit, and observability contracts

Out of scope for MVP

  • Merchant or user administration, registration, password recovery, and browser operator access
  • Arbitrary formulas, date ranges, filters, sorting, cross-merchant drill-down, and peer-row export
  • Payer-token, credential, session-token, raw cache/job, or AI interfaces
  • Asynchronous export jobs, saved reports, scheduled delivery, and export history
  • Production payment writes, CSV ingestion, migrations, deployment endpoints, and AvalAI calls

Protocol conventions

  • HTTPS is required outside explicit local development. Successful structured responses use application/json; errors use application/problem+json.
  • JSON field and query names use camelCase; enum values use UPPER_SNAKE_CASE; canonical metric and section ids retain their approved snake_case values.
  • Canonical timestamps are ISO 8601/RFC 3339 instants in UTC with Z. Period objects also return their Tehran-local half-open boundaries with explicit +03:30 offsets and a ready-to-display Persian/Jalali label.
  • Money is an integer number of Rial. Rates and percentiles are fixed-precision decimal JSON strings, never binary floating-point values.
  • Unknown JSON fields and unknown query parameters are rejected with VALIDATION_ERROR so misspelled filters cannot silently broaden a result.
  • Protected responses use Cache-Control: private, no-store. Responses include X-Request-ID; a safe client-supplied request id may be accepted but never trusted as unique.
  • State-changing browser requests require both the approved CSRF token header and strict same-origin evidence. Login is unauthenticated but still enforces origin checks and its approved throttles.

Resource map

Method Path Purpose
POST /api/v1/auth/sessions Authenticate and create a server-side session
GET /api/v1/auth/session Return the current merchant-user session summary
DELETE /api/v1/auth/session Revoke the current session and clear its cookie
GET /api/v1/analytics/periods List approved period choices and the default period
POST /api/v1/analytics/runs Idempotently create or return the current run
GET /api/v1/analytics/runs/{runId} Return the current run/section snapshot
GET /api/v1/analytics/runs/{runId}/events Stream authorized SSE run events
POST /api/v1/analytics/runs/{runId}/sections/{sectionId}/retry-attempts Retry one exhausted failed section
GET /api/v1/analytics/session-details List authorized merchant-owned session evidence
GET /api/v1/analytics/session-details.csv Export the same authorized detail view as CSV

The request/response schemas, required headers, status codes, examples, and stable operationId values are defined in docs/openapi.yaml.

Reporting-period contract

Clients select one allowlisted periodId returned by GET /analytics/periods; they do not submit arbitrary start/end timestamps. Each period contains:

  • stable periodId and periodType (DAY, WEEK, JALALI_MONTH, or PERSIAN_SEASON);
  • exact half-open UTC query boundaries startAt and endAt;
  • exact Tehran-local boundaries localStartAt and localEndAt;
  • timezone: Asia/Tehran and jalaliLabelFa, ready for display;
  • isComplete and isDefault flags.

The default is the latest complete Jalali month containing source data. Weekly periods start Saturday at 00:00 and end at the following Saturday 00:00 in Tehran. The server rejects stale or fabricated period ids instead of accepting client-computed dates.

Analytics-run contract

POST /analytics/runs accepts periodId and an Idempotency-Key representing one dashboard-start intent. The server binds the key to the authenticated user, merchant, canonical request, merchant/cohort revisions, and metric-definition version.

  • The same key and same canonical request replays the original status/body without duplicate jobs.
  • The same key with a different request returns 422 IDEMPOTENCY_KEY_REUSED.
  • Equivalent concurrent requests for the current revision resolve to the same run even when their valid intent keys differ.
  • An in-flight duplicate returns the current 202 run snapshot rather than starting another effect.
  • Intent records outlive the one-hour event replay window and the maximum job retry chain; the concrete retention value is an implementation parameter that must be documented before implementation.

A run snapshot identifies the selected period, revisions, metric-definition version, creation/update times, overall state, and all four sections. Section states are QUEUED, CALCULATING, READY, FAILED, or SUPPRESSED. A failure includes only a safe stable error code and retry eligibility. Ready section payloads use the typed schemas in OpenAPI. A suppressed peer section contains threshold evidence but no peer metric values.

Metric response rules

  • Every metric value identifies its canonical metric id, unit, definition version, sample/denominator or coverage where applicable, and calculation time through its containing section.
  • Count and money values are JSON integers. Percentage, ratio, and percentile values are decimal strings or null when mathematically undefined.
  • Zero, undefined denominator, unavailable comparison, suppressed peer result, not-yet-ready, and calculation failure are distinct states.
  • Paid and Verified are bank-paid; only Verified is finalized. Session money and fees are counted once regardless of attempt count.
  • Timing values include p50, p95, contributing sample size, and coverage. Missing timings never become zero.
  • Peer outputs include only the approved merchant-weighted cohort median and direction-adjusted merchant percentile after both suppression thresholds pass.

Detail views and cursor pagination

GET /analytics/session-details requires an approved view:

  • UNVERIFIED_PAID: sessions currently in Paid;
  • FAILED: sessions currently in Failed, optionally filtered by failureStage=NO_ATTEMPT|REACHED_BANK|OTHER_OBSERVED;
  • RETRIED: sessions with more than one normalized attempt.

All views require one approved periodId. Optional terminal and observable status/stage filters are validated inside the merchant boundary. There is no merchant-id, peer, payer-token, free-text, response-code, PSP, bank, or arbitrary date filter in the MVP.

Ordering is stable and deterministic: createdAt DESC, sessionId DESC. The opaque cursor binds the merchant, view, canonical filters, ordering, and last row. It must be integrity-protected, expire after a bounded interval, and reveal no internal ids or filter data when decoded by a client.

  • limit defaults to 25 and is limited to 1–100.
  • Responses contain items, pageInfo.limit, pageInfo.hasNextPage, and nullable pageInfo.nextCursor.
  • Total counts are omitted from page responses to avoid an additional unbounded count query. Aggregate cards remain the reconciliation source.
  • Reusing a cursor with changed filters, another merchant, or after expiry returns 400 INVALID_CURSOR without disclosing which binding failed.

Detail rows expose only fields available in the authorized view: opaque merchant-local sessionId, terminal display/reference id, status, amount/fee in Rial, timestamps, attempt count, and a bounded ordered attempt summary where relevant. They exclude source merchant keys, payer tokens, credentials, raw session tokens, peer data, internal database keys, and undocumented response-code meanings.

CSV export

GET /analytics/session-details.csv accepts exactly the same approved view and filters as the JSON detail endpoint, except cursor and page limit. The server reauthorizes and re-resolves the merchant at export time. It executes one immutable logical snapshot so rows do not drift between authorization, limit validation, and streaming.

Format and delivery

  • Content-Type: text/csv; charset=utf-8
  • UTF-8 with one leading BOM for common Persian spreadsheet compatibility
  • RFC 4180-style comma separation, header row, CRLF records, and doubled quotes
  • Stable ASCII snake_case column names; Persian values remain UTF-8
  • UTC ISO timestamps with Z; Rial integer values; no locale-formatted numbers
  • Content-Disposition: attachment with an ASCII filename fallback followed by an RFC 6266 filename*=UTF-8''... value
  • Cache-Control: private, no-store and X-Content-Type-Options: nosniff

The filename contains the view, canonical period id, and generation timestamp. It never contains a username, merchant name/key, raw filter text, or other user-controlled value.

Safeguards

  • Export columns are an allowlisted subset of the equivalent authorized JSON detail view. A CSV never adds payer tokens, credentials, peer-level data, internal ids, or fields unavailable in that view.
  • Text cells that begin with =, +, -, @, tab, CR, or LF after leading whitespace are prefixed with a single apostrophe before CSV quoting to prevent spreadsheet formula execution. Numeric money/count columns are emitted only after strict numeric typing.
  • Embedded CR/LF and quotes are encoded per CSV rules; control characters not required by approved fields are rejected or replaced with a documented safe marker.
  • The export fails before sending CSV bytes if its preflight result exceeds either 100,000 rows or 50 MiB estimated uncompressed output. It returns 413 EXPORT_TOO_LARGE and instructs the merchant to narrow approved filters.
  • Export generation has a 60-second server deadline. A timeout returns a Problem Details response if headers are unsent; a connection terminated after streaming begins is incomplete and must never be presented as a successful audited export.
  • Export requests default to 3 per 10 minutes with burst 1 in independent authenticated-user and merchant buckets. A rejection returns 429 with Retry-After before query/stream work begins.

The row/byte/deadline/export-throttle defaults are proposed MVP policy and are called out for explicit approval under Remaining product questions.

SSE events and recovery

The run stream uses text/event-stream; charset=utf-8, disables intermediary buffering/compression, sends Cache-Control: no-cache, no-transform, and reauthorizes the session for connection and replay. Event ids are opaque, stable within the retained run history, and scoped to the run and merchant.

Event Payload purpose
run.snapshot Full current snapshot on initial connection or replay reset
section.started Section entered CALCULATING
section.ready Typed ready section payload
section.failed Safe failure code and manual-retry eligibility
section.suppressed Peer section suppressed with threshold evidence only
run.completed Terminal run snapshot after every section is terminal
heartbeat Server time and run id; no analytics payload

Each non-comment event contains id, event, and one-line JSON data. Heartbeats are named events every 15 seconds so clients can observe liveness. The server may send SSE comment lines additionally, but clients must not depend on them.

On reconnect, the browser sends Last-Event-ID. If the id remains within the one-hour authorized replay history, the server replays only later events in original order and then continues live. If it is absent, invalid, belongs elsewhere, or has expired, the stream emits a fresh run.snapshot with recovery: RESET_TO_SNAPSHOT and continues from new events; it never reveals whether an id belongs to another merchant. Session expiry/revocation terminates recovery access. A completed run emits its current snapshot and terminal event, then may close.

Error contract

Every non-SSE error response uses RFC 9457 application/problem+json with:

{
  "type": "https://api.zarinpal.example/problems/validation-error",
  "title": "Request validation failed",
  "status": 422,
  "detail": "One or more request values are invalid.",
  "instance": "/api/v1/analytics/runs",
  "code": "VALIDATION_ERROR",
  "requestId": "01J...",
  "errors": [{ "field": "periodId", "reason": "UNKNOWN_PERIOD" }]
}

code is the stable application discriminator; clients must not parse title or detail. requestId is safe correlation metadata. Validation errors use allowlisted field names/reasons and never echo credentials, cookies, cursor contents, SQL, stack traces, peer identities, or sensitive request values.

Status Stable codes (non-exhaustive)
400 MALFORMED_REQUEST, INVALID_CURSOR
401 UNAUTHENTICATED
403 FORBIDDEN, CSRF_FAILED
404 RESOURCE_NOT_FOUND
409 SECTION_NOT_RETRYABLE, RUN_REVISION_STALE
413 REQUEST_TOO_LARGE, EXPORT_TOO_LARGE
415 UNSUPPORTED_MEDIA_TYPE
422 VALIDATION_ERROR, IDEMPOTENCY_KEY_REUSED, UNKNOWN_PERIOD
429 RATE_LIMITED, CONNECTION_LIMIT_REACHED
500 INTERNAL_ERROR
503 DEPENDENCY_UNAVAILABLE, ANALYTICS_UNAVAILABLE
504 ANALYTICS_TIMEOUT, EXPORT_TIMEOUT

For tenant-owned ids, an absent resource and a resource outside the authenticated merchant boundary both return the same 404 RESOURCE_NOT_FOUND. 429 and retryable 503 responses include Retry-After when a meaningful retry time is known.

Authorization and privacy matrix

Surface Authentication Authorization/privacy rule
Login None; origin checked Generic credential outcome and approved username/IP throttles
Current session/logout Valid current session Current user/session only; logout requires CSRF and origin evidence
Periods Valid current session Approved global period definitions; no merchant ids accepted
Run/snapshot/SSE/retry Valid current session Run merchant must equal session merchant; ids never authorize
Detail JSON/CSV Valid current session Query is built inside session merchant scope; export is an allowlisted projection
Peer result Valid current session Aggregate or suppression only; never peer rows/ids/exports

The API never serializes payer_card_key, password hashes, raw session/CSRF tokens, cookie values, credential material, source merchant keys, individual peer values/ranks, Redis keys, job payloads, or AI secrets.

Rate and size limits

Approved existing limits remain unchanged:

  • login: five failures/normalized username and 30 failures/source IP per five minutes;
  • analytics start: 10/minute, burst 3, independently per user and merchant;
  • failed-section retry: 3/10 minutes, burst 1, independently per user and merchant;
  • SSE: two concurrent streams/user and four/merchant;
  • section attempt: 60 seconds and at most two transient retries.

Proposed API boundary defaults:

  • JSON request body: 16 KiB maximum; requests with no defined body reject non-empty bodies;
  • request target/query string: 8 KiB maximum at the public edge;
  • detail page: default 25, maximum 100;
  • CSV: 100,000 rows, 50 MiB estimated uncompressed output, 60 seconds, and 3 requests/10 minutes burst 1 per user and merchant.

Limits are enforced before enqueuing or expensive work where possible. User and merchant buckets are independent; exhausting one never consumes another tenant's allowance.

Audit and observability

Audit events

Record login success/failure category, logout, analytics start/deduplication, manual retry, SSE authorization/replay reset, detail access, and CSV export requested/completed/rejected/aborted. Export audit records include user id, merchant id, canonical view/filter fingerprint, period id, row count, byte count, result, request id, and timestamps—not exported row values, payer tokens, credentials, raw cursors, filenames from users, or peer data.

Minimal demo authentication audit retention remains 30 days. Retention for analytics-access/export audit records is unresolved and must be approved before implementation.

Metrics and traces

Measure request duration/status/code/response size; rate-limit rejections by operation/bucket type; run deduplication; section queue/calculation/cache/retry behavior; SSE open duration, heartbeat, replay/reset, disconnect; detail query duration/page size; and export preflight/query/stream duration, rows, bytes, rejection/abort reason.

Correlate requestId, authenticated user/merchant internal ids, run id, section id, job id, and event id in structured telemetry. Do not place usernames, source ids, session ids from payment data, raw filter values, cookies, CSRF values, payer tokens, or merchant row payloads in metric labels or traces.

Acceptance criteria

  • OpenAPI validates as 3.1 and defines every operation, parameter, body, response, security requirement, SSE event, CSV header, and RFC 9457 problem schema named by this spec.
  • Generated frontend types can consume JSON schemas without handwritten duplicate DTOs; all operationId values and schema names are stable and unique.
  • Every protected endpoint resolves the active merchant from the server-side session and produces the same 404 for absent and cross-tenant resource ids.
  • Login/logout/session behavior, cookie flags, CSRF/origin checks, expiry, revocation, and throttling match the approved Identity spec.
  • Period discovery returns exact UTC and Tehran-local half-open boundaries plus a ready-to-display Persian/Jalali label; arbitrary ranges are rejected.
  • Equivalent analytics starts deduplicate, idempotency-key payload conflicts fail, and all four section states/results match the Analytics spec and metric catalogue.
  • SSE delivers independently completed sections, 15-second heartbeats, ordered one-hour replay, snapshot reset after replay loss, sibling failure isolation, and reauthorization after session changes.
  • Detail pagination is deterministic, defaults to 25, caps at 100, and rejects a cursor reused across filters or tenant boundaries.
  • Each detail view reconciles with its approved aggregate and exposes no payer token, credential, peer row, undocumented cause, or field unavailable to that view.
  • CSV applies the same tenant/filter predicate and allowlisted projection as JSON detail, uses the documented UTF-8/CSV/content-disposition contract, neutralizes spreadsheet formulas, and emits no partial success audit on interruption.
  • Request/rate/export limits reject before expensive work and return Problem Details plus Retry-After where applicable.
  • Problem responses keep HTTP status and status aligned, expose stable code, and leak no stack, SQL, secret, cursor content, peer identity, or cross-tenant existence.
  • Contract/security tests cover schema examples, unknown fields/params, cross-tenant ids and cursors, stale event ids, idempotency races, export filter parity, formula injection, row/byte/deadline caps, and sensitive-field absence.

Boundaries

  • Always: treat OpenAPI as source of truth; derive tenant scope from the session; validate inputs at the HTTP boundary; preserve approved metric/period semantics; use RFC 9457; paginate detail lists; reauthorize streams/exports; audit sensitive actions without payloads.
  • Ask first: change an endpoint/schema/error code, expose a new field/filter/detail/export, change a rate/size/retention limit, add a role, change cookie/CORS/CSRF behavior, or add a cross-service/public interface.
  • Never: accept merchant id as authority; expose payer tokens, credentials, peer rows, internal errors, or raw infrastructure state; silently broaden invalid filters; return partial CSV as success; let OpenAPI drift behind implementation.

Remaining product questions

  1. Approve or revise the proposed CSV policy: synchronous export, 100,000-row and 50-MiB uncompressed caps, 60-second deadline, and 3 requests/10 minutes with burst 1 per user and merchant.
  2. Approve UTF-8 BOM output for spreadsheet compatibility and stable ASCII column headers, or require localized Persian header names.
  3. Define retention and operator access policy for analytics-access and CSV-export audit records; Identity's 30-day demo-auth retention does not automatically cover these events.
  4. Confirm whether terminal identifiers may be shown/exported as their existing opaque source reference or require a separate merchant-facing display alias.
  5. Choose the production base URI used for dereferenceable Problem Details type documentation; https://api.zarinpal.example/problems/... is a non-production placeholder in the draft contract.

Standards references