openapi: 3.1.0 info: title: ZarinPal Merchant Analytics API version: 1.1.0 summary: Contract-first API for authenticated merchant analytics and growth reports. description: | Approved analytics and AI growth-report interface. This document is the API source of truth; generated frontend types must come from this contract. Merchant scope is always derived from the authenticated server-side session. No operation accepts a merchant identifier as authorization. All timestamps are ISO 8601. Money values are integer Rial, and decimal rates are strings. license: name: Proprietary servers: - url: /api/v1 description: Same-origin API base tags: - name: Authentication - name: Reporting periods - name: Analytics runs - name: Analytics details security: - merchantSession: [] paths: /auth/sessions: post: tags: [Authentication] operationId: createAuthSession summary: Authenticate a demo merchant user description: | Creates a revocable server-side session. Expected credential failures are externally indistinguishable. The response sets the host-only `__Host-zpa_session` cookie; the raw token is never present in JSON. security: [] parameters: - $ref: '#/components/parameters/OriginHeader' - $ref: '#/components/parameters/RequestIdHeader' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/LoginRequest' responses: '201': description: Authenticated session created. headers: Set-Cookie: description: Secure opaque session cookie; value is intentionally omitted from examples. schema: type: string example: __Host-zpa_session=; Path=/; Secure; HttpOnly; SameSite=Lax X-Request-ID: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/AuthSession' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthenticated' '413': $ref: '#/components/responses/RequestTooLarge' '415': $ref: '#/components/responses/UnsupportedMediaType' '422': $ref: '#/components/responses/ValidationFailed' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/DependencyUnavailable' /auth/session: get: tags: [Authentication] operationId: getAuthSession summary: Get the current session summary parameters: - $ref: '#/components/parameters/RequestIdHeader' responses: '200': description: Current authenticated merchant-user session. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/AuthSession' '401': $ref: '#/components/responses/Unauthenticated' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/DependencyUnavailable' delete: tags: [Authentication] operationId: deleteAuthSession summary: Revoke the current session description: Revokes server-side state before expiring the browser cookie. parameters: - $ref: '#/components/parameters/CsrfTokenHeader' - $ref: '#/components/parameters/OriginHeader' - $ref: '#/components/parameters/RequestIdHeader' responses: '204': description: Current authenticated session revoked. headers: Set-Cookie: description: Expired session cookie. schema: type: string example: __Host-zpa_session=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Lax X-Request-ID: $ref: '#/components/headers/RequestId' '401': $ref: '#/components/responses/Unauthenticated' '403': $ref: '#/components/responses/Forbidden' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/DependencyUnavailable' /analytics/periods: get: tags: [Reporting periods] operationId: listAnalyticsPeriods summary: List approved reporting periods description: | Returns every complete allowlisted day, Iranian week, Jalali month, and Persian season inside the imported dataset's local-date coverage, with server-calculated UTC and Tehran-local half-open boundaries. Periods are grouped by type and ordered newest-first inside each group. Clients start runs with a returned `periodId` and never submit arbitrary timestamps. parameters: - name: periodType in: query required: false description: Optional allowlisted period type. schema: $ref: '#/components/schemas/PeriodType' - $ref: '#/components/parameters/RequestIdHeader' responses: '200': description: Approved period choices and default selection. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/PeriodList' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthenticated' '422': $ref: '#/components/responses/ValidationFailed' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/DependencyUnavailable' /analytics/terminals: get: tags: [Reporting periods] operationId: listMerchantTerminals summary: List terminals owned by the authenticated merchant description: Returns display-safe terminal choices scoped exclusively from the authenticated server session. parameters: - $ref: '#/components/parameters/RequestIdHeader' responses: '200': description: Merchant-owned terminal choices ordered by session volume. content: application/json: schema: $ref: '#/components/schemas/TerminalList' '401': $ref: '#/components/responses/Unauthenticated' '503': $ref: '#/components/responses/AnalyticsUnavailable' /analytics/runs: post: tags: [Analytics runs] operationId: createAnalyticsRun summary: Create or return the current analytics run description: | Idempotently resolves the current merchant/dataset/metric version and starts only missing sections. Equivalent concurrent starts deduplicate. parameters: - $ref: '#/components/parameters/IdempotencyKeyHeader' - $ref: '#/components/parameters/CsrfTokenHeader' - $ref: '#/components/parameters/OriginHeader' - $ref: '#/components/parameters/RequestIdHeader' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateAnalyticsRunRequest' responses: '200': description: Existing current run returned with no new work required. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' Idempotency-Replayed: description: Whether this response replayed a prior intent result. schema: type: boolean X-Request-ID: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/AnalyticsRun' '202': description: Current run accepted and one or more missing sections are queued/calculating. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' Idempotency-Replayed: description: Whether this response replayed a prior intent result. schema: type: boolean Location: description: Relative URL of the run snapshot. schema: type: string format: uri-reference X-Request-ID: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/AnalyticsRun' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthenticated' '403': $ref: '#/components/responses/Forbidden' '413': $ref: '#/components/responses/RequestTooLarge' '415': $ref: '#/components/responses/UnsupportedMediaType' '422': $ref: '#/components/responses/ValidationFailed' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/AnalyticsUnavailable' /analytics/runs/{runId}: parameters: - $ref: '#/components/parameters/RunIdPath' get: tags: [Analytics runs] operationId: getAnalyticsRun summary: Get the current run snapshot parameters: - $ref: '#/components/parameters/RequestIdHeader' responses: '200': description: Current state of every run section. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/AnalyticsRun' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthenticated' '404': $ref: '#/components/responses/ResourceNotFound' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/AnalyticsUnavailable' /analytics/runs/{runId}/events: parameters: - $ref: '#/components/parameters/RunIdPath' get: tags: [Analytics runs] operationId: streamAnalyticsRunEvents summary: Stream partial analytics results description: | Authenticated SSE stream. Events are retained for one hour. A retained `Last-Event-ID` replays only later events. Missing, invalid, unauthorized, or expired ids reset safely by emitting `run.snapshot` with `recovery=RESET_TO_SNAPSHOT`, then continuing from new events. Event types are `run.snapshot`, `section.started`, `section.ready`, `section.failed`, `section.suppressed`, `run.completed`, and `heartbeat`. Each event has an opaque `id` and one-line JSON `data` matching one of the schemas named by `AnalyticsSseEvent`. parameters: - name: Last-Event-ID in: header required: false description: Opaque last processed event id; must not contain NUL, LF, or CR. schema: type: string minLength: 1 maxLength: 256 pattern: '^[^\u0000\u000A\u000D]+$' - $ref: '#/components/parameters/RequestIdHeader' responses: '200': description: UTF-8 event stream. A named heartbeat is emitted every 15 seconds. headers: Cache-Control: description: Prevents transformation or storage of the live event stream. schema: type: string const: no-cache, no-transform X-Accel-Buffering: description: Deployment hint disabling Nginx response buffering. schema: type: string const: 'no' X-Request-ID: $ref: '#/components/headers/RequestId' content: text/event-stream: schema: type: string contentMediaType: text/event-stream examples: sectionReady: summary: One ready-section event value: |- id: evt_01K2M7ZZZZZZZZZZZZZZZZZZZZ event: section.ready data: {"eventId":"evt_01K2M7ZZZZZZZZZZZZZZZZZZZZ","runId":"run_01K2M7YYYYYYYYYYYYYYYYYYYY","occurredAt":"2026-08-20T09:30:00Z","sectionId":"overview","section":{"sectionId":"overview","state":"READY","calculatedAt":"2026-08-20T09:30:00Z","data":{}}} '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthenticated' '404': $ref: '#/components/responses/ResourceNotFound' '429': $ref: '#/components/responses/ConnectionLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/AnalyticsUnavailable' /analytics/runs/{runId}/sections/{sectionId}/retry-attempts: parameters: - $ref: '#/components/parameters/RunIdPath' - $ref: '#/components/parameters/SectionIdPath' post: tags: [Analytics runs] operationId: createAnalyticsSectionRetryAttempt summary: Retry one exhausted failed section description: Does not clear or restart ready sibling sections. parameters: - $ref: '#/components/parameters/IdempotencyKeyHeader' - $ref: '#/components/parameters/CsrfTokenHeader' - $ref: '#/components/parameters/OriginHeader' - $ref: '#/components/parameters/RequestIdHeader' responses: '202': description: Retry accepted or equivalent current retry returned. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' Location: description: Relative run snapshot URL. schema: type: string format: uri-reference X-Request-ID: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/AnalyticsRun' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthenticated' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/ResourceNotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/ValidationFailed' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/AnalyticsUnavailable' /analytics/session-details: get: tags: [Analytics details] operationId: listAnalyticsSessionDetails summary: List authorized merchant-owned session evidence description: | Cursor-paginated detail rows for one approved period and view. Stable ordering is `createdAt DESC, sessionId DESC`. The server binds cursors to merchant, filters, and ordering. Cross-tenant and absent ids are not distinguishable. parameters: - $ref: '#/components/parameters/PeriodIdQuery' - $ref: '#/components/parameters/DetailViewQuery' - $ref: '#/components/parameters/FailureStageQuery' - $ref: '#/components/parameters/TerminalIdQuery' - $ref: '#/components/parameters/SessionStatusQuery' - $ref: '#/components/parameters/MinAmountRialQuery' - $ref: '#/components/parameters/MaxAmountRialQuery' - $ref: '#/components/parameters/CursorQuery' - $ref: '#/components/parameters/LimitQuery' - $ref: '#/components/parameters/RequestIdHeader' responses: '200': description: Authorized detail page. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/SessionDetailPage' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthenticated' '404': $ref: '#/components/responses/ResourceNotFound' '422': $ref: '#/components/responses/ValidationFailed' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/AnalyticsUnavailable' /analytics/sessions/{sessionId}: get: tags: [Analytics details] operationId: getAnalyticsSession summary: Get one merchant-owned payment session and attempt history parameters: - name: sessionId in: path required: true schema: type: string minLength: 8 maxLength: 128 - $ref: '#/components/parameters/RequestIdHeader' responses: '200': description: Authorized merchant-owned session. content: application/json: schema: $ref: '#/components/schemas/SessionDetail' '401': $ref: '#/components/responses/Unauthenticated' '404': $ref: '#/components/responses/ResourceNotFound' '503': $ref: '#/components/responses/AnalyticsUnavailable' /analytics/session-details.csv: get: tags: [Analytics details] operationId: exportAnalyticsSessionDetailsCsv summary: Export the authorized detail view as CSV description: | Uses the same tenant scope, approved period, filters, and allowlisted projection as `listAnalyticsSessionDetails`. Cursor and page limit do not apply. Output is UTF-8 with a BOM, a Persian header row whose labels use normal spaces, comma separators, CRLF records, and RFC 4180 quoting. Stable columns are: `شناسه نشست پرداخت`, `شناسه پایانه`, `وضعیت پرداخت`, `مبلغ ریال`, `کارمزد ریال`, `زمان ایجاد`, `زمان انقضا`, `تعداد تلاش`, `وضعیت آخرین تلاش`, `زمان آخرین تلاش`, `زمان تأیید`, `زمان تسویه`, `مرحله مشاهده شده شکست`. CSV status labels are Persian: `Paid` is `پرداخت شده و تأیید نشده`, `Verified` is `تأیید شده`, `Failed` is `ناموفق`, `Reversed` is `برگشت خورده`, and `InBank` is `در بانک`. Numeric cells remain plain calculation-safe numbers and timestamps remain UTC ISO values. Columns unavailable to the chosen authorized view are empty. Payer tokens, credentials, peer data, internal ids, and any field unavailable in the JSON detail view are forbidden. Potential spreadsheet formulas in textual cells are prefixed with an apostrophe before CSV quoting. parameters: - $ref: '#/components/parameters/PeriodIdQuery' - $ref: '#/components/parameters/DetailViewQuery' - $ref: '#/components/parameters/FailureStageQuery' - $ref: '#/components/parameters/TerminalIdQuery' - $ref: '#/components/parameters/SessionStatusQuery' - $ref: '#/components/parameters/MinAmountRialQuery' - $ref: '#/components/parameters/MaxAmountRialQuery' - $ref: '#/components/parameters/RequestIdHeader' responses: '200': description: Complete authorized CSV export. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' Content-Disposition: description: | Attachment with ASCII fallback first and UTF-8 `filename*` second. Filename contains only server-generated view, period id, and time. schema: type: string example: attachment; filename="unverified_paid-jm-1405-05-20260820T093000Z.csv"; filename*=UTF-8''unverified_paid-jm-1405-05-20260820T093000Z.csv X-Content-Type-Options: description: Prevents content sniffing. schema: type: string const: nosniff X-Request-ID: $ref: '#/components/headers/RequestId' content: text/csv; charset=utf-8: schema: type: string contentMediaType: text/csv example: "\uFEFF\"شناسه نشست پرداخت\",\"شناسه پایانه\",\"وضعیت پرداخت\",\"مبلغ ریال\",\"کارمزد ریال\",\"زمان ایجاد\",\"زمان انقضا\",\"تعداد تلاش\",\"وضعیت آخرین تلاش\",\"زمان آخرین تلاش\",\"زمان تأیید\",\"زمان تسویه\",\"مرحله مشاهده شده شکست\"\r\nses_01K2M8,term_0042,پرداخت شده و تأیید نشده,1500000,2000,2026-06-01T08:00:00Z,2026-06-01T08:15:00Z,1,پرداخت شده و تأیید نشده,2026-06-01T08:02:00Z,,,\r\n" '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthenticated' '404': $ref: '#/components/responses/ResourceNotFound' '413': $ref: '#/components/responses/ExportTooLarge' '422': $ref: '#/components/responses/ValidationFailed' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/AnalyticsUnavailable' '504': $ref: '#/components/responses/ExportTimeout' /analytics/growth-reports: post: tags: [AI growth analyst] operationId: createGrowthReport summary: Create or reuse a growth report parameters: - $ref: '#/components/parameters/IdempotencyKeyHeader' - $ref: '#/components/parameters/CsrfTokenHeader' - $ref: '#/components/parameters/OriginHeader' - $ref: '#/components/parameters/RequestIdHeader' requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/CreateGrowthReportRequest'} responses: '200': {description: Matching durable report reused., content: {application/json: {schema: {$ref: '#/components/schemas/GrowthReport'}}}} '202': {description: Report accepted for generation., content: {application/json: {schema: {$ref: '#/components/schemas/GrowthReport'}}}} '401': {$ref: '#/components/responses/Unauthenticated'} '403': {$ref: '#/components/responses/Forbidden'} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationFailed'} '429': {$ref: '#/components/responses/RateLimited'} '503': {$ref: '#/components/responses/AnalyticsUnavailable'} /analytics/growth-reports/latest: get: tags: [AI growth analyst] operationId: getLatestGrowthReport parameters: - {name: terminalId, in: query, required: false, schema: {type: string}} - $ref: '#/components/parameters/RequestIdHeader' responses: '200': {description: Latest successful report for the authorized scope., content: {application/json: {schema: {$ref: '#/components/schemas/GrowthReport'}}}} '401': {$ref: '#/components/responses/Unauthenticated'} '404': {$ref: '#/components/responses/ResourceNotFound'} '503': {$ref: '#/components/responses/AnalyticsUnavailable'} /analytics/growth-reports/{reportId}: get: tags: [AI growth analyst] operationId: getGrowthReport parameters: - {name: reportId, in: path, required: true, schema: {type: string, pattern: '^growth_[a-f0-9]{24}$'}} - $ref: '#/components/parameters/RequestIdHeader' responses: '200': {description: Authorized report snapshot., content: {application/json: {schema: {$ref: '#/components/schemas/GrowthReport'}}}} '401': {$ref: '#/components/responses/Unauthenticated'} '404': {$ref: '#/components/responses/ResourceNotFound'} '503': {$ref: '#/components/responses/AnalyticsUnavailable'} /analytics/growth-reports/{reportId}/events: get: tags: [AI growth analyst] operationId: streamGrowthReportEvents parameters: - {name: reportId, in: path, required: true, schema: {type: string}} - {name: Last-Event-ID, in: header, required: false, schema: {type: string}} responses: '200': {description: Stable-id report snapshots and heartbeats., content: {text/event-stream: {schema: {type: string}}}} '401': {$ref: '#/components/responses/Unauthenticated'} '404': {$ref: '#/components/responses/ResourceNotFound'} components: securitySchemes: merchantSession: type: apiKey in: cookie name: __Host-zpa_session description: | Opaque server-side session token. The browser receives it only as a Secure, HttpOnly, SameSite=Lax, host-only cookie with Path=/. parameters: RunIdPath: name: runId in: path required: true description: Opaque analytics run identifier; possession does not authorize access. schema: $ref: '#/components/schemas/RunId' SectionIdPath: name: sectionId in: path required: true schema: $ref: '#/components/schemas/SectionId' IdempotencyKeyHeader: name: Idempotency-Key in: header required: true description: Client-generated intent key reused unchanged for retries of one intent. schema: type: string minLength: 8 maxLength: 128 pattern: '^[\x21-\x7E]+$' example: 95f6ec2d-26e8-4983-afd4-8a6d4434726b CsrfTokenHeader: name: X-CSRF-Token in: header required: true description: CSRF token for the current authenticated session; never log this value. schema: type: string minLength: 32 maxLength: 256 OriginHeader: name: Origin in: header required: true description: Strict same-origin evidence validated with Fetch Metadata where available. schema: type: string format: uri RequestIdHeader: name: X-Request-ID in: header required: false description: Optional safe correlation id. The server may replace invalid values. schema: type: string minLength: 8 maxLength: 128 pattern: '^[A-Za-z0-9._:-]+$' PeriodIdQuery: name: periodId in: query required: true description: Stable id returned by `listAnalyticsPeriods`. schema: $ref: '#/components/schemas/PeriodId' DetailViewQuery: name: view in: query required: true schema: $ref: '#/components/schemas/DetailView' FailureStageQuery: name: failureStage in: query required: false description: Valid only when `view=FAILED`. schema: $ref: '#/components/schemas/FailureStage' TerminalIdQuery: name: terminalId in: query required: false description: Approved opaque source terminal reference resolved only inside the authenticated merchant; never an authorization input. schema: type: string minLength: 1 maxLength: 128 pattern: '^[A-Za-z0-9._:-]+$' SessionStatusQuery: name: status in: query required: false schema: $ref: '#/components/schemas/SessionStatus' MinAmountRialQuery: name: minAmountRial in: query required: false schema: type: integer format: int64 minimum: 0 MaxAmountRialQuery: name: maxAmountRial in: query required: false schema: type: integer format: int64 minimum: 0 CursorQuery: name: cursor in: query required: false description: Opaque integrity-protected cursor bound to tenant, view, filters, and ordering. schema: type: string minLength: 16 maxLength: 2048 LimitQuery: name: limit in: query required: false description: Page size; defaults to 25 and cannot exceed 100. schema: type: integer minimum: 1 maximum: 100 default: 25 headers: RequestId: description: Server correlation id for this request. schema: type: string minLength: 8 maxLength: 128 PrivateNoStore: description: Prevents storage of authenticated merchant data. schema: type: string const: private, no-store RetryAfter: description: Seconds until retry is permitted. schema: type: integer minimum: 1 responses: BadRequest: description: Request syntax, cursor, event id, or parameter combination is invalid. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' Unauthenticated: description: The current session is missing, malformed, expired, or revoked. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: https://zarinpal.amiirkhl.ir/docs/errors/unauthenticated title: Authentication required status: 401 detail: Sign in to continue. instance: /api/v1/analytics/runs code: UNAUTHENTICATED requestId: req_01K2M8A0000000000000000000 Forbidden: description: The authenticated request is not permitted or failed CSRF/origin checks. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' ResourceNotFound: description: Resource is absent or is outside the authenticated merchant boundary. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: https://zarinpal.amiirkhl.ir/docs/errors/resource-not-found title: Resource not found status: 404 detail: The requested resource is unavailable. instance: /api/v1/analytics/runs/run_unknown code: RESOURCE_NOT_FOUND requestId: req_01K2M8A0000000000000000000 Conflict: description: Current resource state does not permit the requested operation. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' RequestTooLarge: description: Request exceeds the 16 KiB JSON body limit. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' ExportTooLarge: description: Export exceeds 100,000 rows or 50 MiB estimated uncompressed output. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: https://zarinpal.amiirkhl.ir/docs/errors/export-too-large title: Export is too large status: 413 detail: Narrow the approved filters and try again. instance: /api/v1/analytics/session-details.csv code: EXPORT_TOO_LARGE requestId: req_01K2M8A0000000000000000000 UnsupportedMediaType: description: Request media type is not supported. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' ValidationFailed: description: Request is well-formed but violates the interface contract. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' example: type: https://zarinpal.amiirkhl.ir/docs/errors/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: req_01K2M8A0000000000000000000 errors: - field: periodId reason: UNKNOWN_PERIOD RateLimited: description: Authenticated-user or merchant rate limit reached before work began. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' Retry-After: $ref: '#/components/headers/RetryAfter' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' ConnectionLimited: description: SSE concurrency limit reached for the user or merchant. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' Retry-After: $ref: '#/components/headers/RetryAfter' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' InternalError: description: Unexpected server failure; implementation details are not exposed. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' DependencyUnavailable: description: Required authoritative dependency is unavailable; protected requests fail closed. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' Retry-After: $ref: '#/components/headers/RetryAfter' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' AnalyticsUnavailable: description: Analytics dependency is temporarily unavailable. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' Retry-After: $ref: '#/components/headers/RetryAfter' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' ExportTimeout: description: Export did not complete within the 60-second deadline before streaming began. headers: Cache-Control: $ref: '#/components/headers/PrivateNoStore' Retry-After: $ref: '#/components/headers/RetryAfter' X-Request-ID: $ref: '#/components/headers/RequestId' content: application/problem+json: schema: $ref: '#/components/schemas/Problem' schemas: CreateGrowthReportRequest: type: object additionalProperties: false properties: terminalId: {type: string, minLength: 1, maxLength: 128} refresh: type: boolean description: When omitted, the request reuses the current matching report. Manual retries remain available until the requested scope has persisted AI copy. GrowthRange: type: object additionalProperties: false required: [p10, p50, p90] properties: p10: {type: integer, format: int64, minimum: 0} p50: {type: integer, format: int64, minimum: 0} p90: {type: integer, format: int64, minimum: 0} GrowthForecastPoint: type: object additionalProperties: false required: [date, baselineSessions, baselineAmountRial, paidSessions, paidAmountRial] properties: date: {type: string, format: date-time} baselineSessions: {type: integer, format: int64, minimum: 0} baselineAmountRial: {type: integer, format: int64, minimum: 0} paidSessions: {$ref: '#/components/schemas/GrowthRange'} paidAmountRial: {$ref: '#/components/schemas/GrowthRange'} eventIds: {type: array, items: {type: string}} GrowthForecast: type: object additionalProperties: false required: [startDate, horizonDays, confidence, points, backtest] properties: startDate: {type: string, format: date-time} horizonDays: {type: integer, enum: [90]} confidence: {type: string, enum: [HIGH, MEDIUM, LOW, INSUFFICIENT_DATA]} suppressionReason: {type: string} points: {type: array, maxItems: 90, items: {$ref: '#/components/schemas/GrowthForecastPoint'}} backtest: type: object additionalProperties: false required: [origins, sessionMapeBps, amountMapeBps, intervalCoverageBps] properties: origins: {type: integer, minimum: 0} sessionMapeBps: {type: integer, format: int64, minimum: 0} amountMapeBps: {type: integer, format: int64, minimum: 0} intervalCoverageBps: {type: integer, format: int64, minimum: 0, maximum: 10000} GrowthHistoricalWin: type: object additionalProperties: false required: [evidenceId, eventId, eventTitleFa, startDate, endDate, materialWin] properties: evidenceId: {type: string} eventId: {type: string} eventTitleFa: {type: string} startDate: {type: string, format: date-time} endDate: {type: string, format: date-time} eligible: {type: boolean} suppressionReason: {type: string} observationCount: {type: integer} baselineSessionCount: {type: integer, format: int64} baselinePaidSessions: {type: integer, format: int64} baselineAmountRial: {type: integer, format: int64} actualSessionCount: {type: integer, format: int64} actualPaidSessions: {type: integer, format: int64} actualAmountRial: {type: integer, format: int64} sessionUpliftBps: {type: integer, format: int64} paidSessionUpliftBps: {type: integer, format: int64} amountUpliftBps: {type: integer, format: int64} materialWin: {type: boolean} materialDecline: {type: boolean} GrowthOpportunity: type: object additionalProperties: false required: [evidenceId, eventId, eventTitleFa, date, similarityBps, expectedSessions, expectedAmountRial, historicalEvidenceId] properties: evidenceId: {type: string} eventId: {type: string} eventTitleFa: {type: string} date: {type: string, format: date-time} similarityBps: {type: integer, format: int64, minimum: 6000, maximum: 10000} expectedSessions: {$ref: '#/components/schemas/GrowthRange'} expectedAmountRial: {$ref: '#/components/schemas/GrowthRange'} historicalEvidenceId: {type: string} GrowthRisk: type: object additionalProperties: false required: [evidenceId, date, sessionDeclineBps, amountDeclineBps, expectedSessions, expectedAmountRial] properties: evidenceId: {type: string} date: {type: string, format: date-time} endDate: {type: string, format: date-time} sessionDeclineBps: {type: integer, format: int64, minimum: 0, description: At least one decline dimension is 2000 BPS or greater.} amountDeclineBps: {type: integer, format: int64, minimum: 0, description: At least one decline dimension is 2000 BPS or greater.} expectedSessions: {$ref: '#/components/schemas/GrowthRange'} expectedAmountRial: {$ref: '#/components/schemas/GrowthRange'} GrowthPeer: type: object additionalProperties: false required: [state, eligiblePeerCount, combinedPeerSessionCount, medianPaidSessions, percentileBps] properties: state: {type: string, enum: [READY, SUPPRESSED]} reason: {type: string} eligiblePeerCount: {type: integer, minimum: 0} combinedPeerSessionCount: {type: integer, format: int64, minimum: 0} medianPaidSessions: {type: integer, format: int64, minimum: 0} percentileBps: {type: integer, format: int64, minimum: 0, maximum: 10000} GrowthCampaignBrief: type: object additionalProperties: false required: [headline, interpretation, offerSuggestion, messageSuggestion, preparationAction, limitation, evidenceIds] properties: headline: {type: string} interpretation: {type: string} offerSuggestion: {type: string} messageSuggestion: {type: string} preparationAction: {type: string} limitation: {type: string} evidenceIds: {type: array, minItems: 1, items: {type: string}} GrowthFallbackBrief: type: object additionalProperties: false required: [headlineFa, guidanceFa, offerFa, messageFa, preparationFa, limitationFa, evidenceIds] properties: headlineFa: {type: string} guidanceFa: {type: string} offerFa: {type: string} messageFa: {type: string} preparationFa: {type: string} limitationFa: {type: string} evidenceIds: {type: array, minItems: 1, items: {type: string}} GrowthEvidence: type: object additionalProperties: false required: [forecast, historicalWins, opportunities, risks, fallbackBriefs] properties: forecast: {$ref: '#/components/schemas/GrowthForecast'} historicalWins: {type: array, items: {$ref: '#/components/schemas/GrowthHistoricalWin'}} opportunities: {type: array, maxItems: 5, items: {$ref: '#/components/schemas/GrowthOpportunity'}} risks: {type: array, maxItems: 3, items: {$ref: '#/components/schemas/GrowthRisk'}} peer: {$ref: '#/components/schemas/GrowthPeer'} fallbackBriefs: {type: array, items: {$ref: '#/components/schemas/GrowthFallbackBrief'}} GrowthReport: type: object additionalProperties: false required: [reportId, revision, status, scope, horizonDays, cached, stale, nextRefreshAt, createdAt, updatedAt, eventsUrl] properties: reportId: {type: string} revision: {type: integer, minimum: 1} status: {type: string, enum: [QUEUED, ANALYZING, GENERATING, READY, PARTIAL, FAILED]} scope: type: object additionalProperties: false properties: {terminalId: {type: [string, 'null']}} horizonDays: {type: integer, enum: [90]} forecastStartDate: {type: string, format: date-time} deterministicEvidence: {$ref: '#/components/schemas/GrowthEvidence'} aiCopy: type: object additionalProperties: false required: [briefs] properties: {briefs: {type: array, maxItems: 5, items: {$ref: '#/components/schemas/GrowthCampaignBrief'}}} failureCode: {type: string} cached: {type: boolean} stale: {type: boolean} nextRefreshAt: type: string format: date-time description: Current time while no AI summary exists for the scope; otherwise the next refresh boundary for that terminal scope in the Tehran day. The all-terminals selection is a separate scope. createdAt: {type: string, format: date-time} updatedAt: {type: string, format: date-time} eventsUrl: {type: string, format: uri-reference} LoginRequest: type: object additionalProperties: false required: [username, password] properties: username: type: string minLength: 1 maxLength: 64 pattern: '^[A-Za-z0-9-]+$' example: merchant-0001 password: type: string minLength: 12 maxLength: 256 writeOnly: true AuthSession: type: object additionalProperties: false required: - user - merchant - createdAt - idleExpiresAt - absoluteExpiresAt - csrfToken properties: user: $ref: '#/components/schemas/MerchantUserSummary' merchant: $ref: '#/components/schemas/MerchantSummary' createdAt: type: string format: date-time idleExpiresAt: type: string format: date-time absoluteExpiresAt: type: string format: date-time csrfToken: type: string description: Session-bound CSRF token. It is not an authentication token and must not be logged. minLength: 32 maxLength: 256 MerchantUserSummary: type: object additionalProperties: false required: [userId, username] properties: userId: type: string minLength: 8 maxLength: 128 username: type: string example: merchant-0001 MerchantSummary: type: object additionalProperties: false required: [merchantId, displayName, profileIsSynthetic] properties: merchantId: type: string description: Opaque API identifier, never accepted as authorization input. minLength: 8 maxLength: 128 displayName: type: string minLength: 1 maxLength: 200 profileIsSynthetic: type: boolean PeriodType: type: string enum: [DAY, WEEK, JALALI_MONTH, PERSIAN_SEASON] PeriodId: type: string minLength: 4 maxLength: 64 pattern: '^[a-z0-9-]+$' example: jm-1405-05 ReportingPeriod: type: object additionalProperties: false required: - periodId - periodType - startAt - endAt - localStartAt - localEndAt - timezone - jalaliLabelFa - isComplete - isDefault properties: periodId: $ref: '#/components/schemas/PeriodId' periodType: $ref: '#/components/schemas/PeriodType' startAt: type: string format: date-time description: Inclusive UTC boundary. example: '2026-07-23T20:30:00Z' endAt: type: string format: date-time description: Exclusive UTC boundary. example: '2026-08-22T20:30:00Z' localStartAt: type: string format: date-time description: Inclusive Asia/Tehran civil boundary with explicit offset. example: '2026-07-24T00:00:00+03:30' localEndAt: type: string format: date-time description: Exclusive Asia/Tehran civil boundary with explicit offset. example: '2026-08-23T00:00:00+03:30' timezone: type: string const: Asia/Tehran jalaliLabelFa: type: string minLength: 1 maxLength: 100 example: مرداد ۱۴۰۵ isComplete: type: boolean isDefault: type: boolean PeriodList: type: object additionalProperties: false required: [defaultPeriodId, periods] properties: defaultPeriodId: $ref: '#/components/schemas/PeriodId' periods: type: array description: Dataset-bounded complete periods, grouped by type and newest-first within each group. maxItems: 1000 items: $ref: '#/components/schemas/ReportingPeriod' CreateAnalyticsRunRequest: type: object additionalProperties: false required: [periodId] properties: periodId: $ref: '#/components/schemas/PeriodId' terminalId: type: string minLength: 1 maxLength: 128 pattern: '^[A-Za-z0-9._:-]+$' Terminal: type: object additionalProperties: false required: [terminalId, displayName, sessionCount, isDefault] properties: terminalId: type: string minLength: 1 maxLength: 128 displayName: type: string minLength: 1 maxLength: 80 sessionCount: type: integer format: int64 minimum: 0 isDefault: type: boolean TerminalList: type: object additionalProperties: false required: [terminals] properties: terminals: type: array maxItems: 1000 items: $ref: '#/components/schemas/Terminal' AnalyticsScope: type: object additionalProperties: false required: [terminalId] properties: terminalId: type: [string, 'null'] minLength: 1 maxLength: 128 RunId: type: string minLength: 12 maxLength: 128 pattern: '^run_[A-Za-z0-9]+$' example: run_01K2M7YYYYYYYYYYYYYYYYYYYY SectionId: type: string enum: [overview, payment_performance, fees, peer_comparison] AnalyticsRunState: type: string enum: [IN_PROGRESS, COMPLETE, COMPLETE_WITH_FAILURES] SectionState: type: string enum: [QUEUED, CALCULATING, READY, FAILED, SUPPRESSED] AnalyticsRun: type: object additionalProperties: false required: - runId - state - period - datasetSnapshotId - metricDefinitionVersion - createdAt - updatedAt - eventsUrl - scope - sections properties: runId: $ref: '#/components/schemas/RunId' state: $ref: '#/components/schemas/AnalyticsRunState' period: $ref: '#/components/schemas/ReportingPeriod' datasetSnapshotId: type: string pattern: '^ds_[A-Za-z0-9_-]{8,60}$' description: Opaque identifier for the immutable completed-import snapshot. metricDefinitionVersion: type: string const: 0.4.0 createdAt: type: string format: date-time updatedAt: type: string format: date-time eventsUrl: type: string format: uri-reference scope: $ref: '#/components/schemas/AnalyticsScope' sections: type: object additionalProperties: false required: [overview, paymentPerformance, fees, peerComparison] properties: overview: $ref: '#/components/schemas/OverviewSection' paymentPerformance: $ref: '#/components/schemas/PaymentPerformanceSection' fees: $ref: '#/components/schemas/FeesSection' peerComparison: $ref: '#/components/schemas/PeerComparisonSection' QueuedSection: type: object additionalProperties: false required: [state] properties: state: type: string const: QUEUED queuedAt: type: [string, 'null'] format: date-time CalculatingSection: type: object additionalProperties: false required: [state, startedAt] properties: state: type: string const: CALCULATING startedAt: type: string format: date-time FailedSection: type: object additionalProperties: false required: [state, failedAt, errorCode, canRetry] properties: state: type: string const: FAILED failedAt: type: string format: date-time errorCode: type: string enum: [CALCULATION_FAILED, CALCULATION_TIMEOUT, SOURCE_UNAVAILABLE, UNSUPPORTED_METRIC_VERSION] canRetry: type: boolean retryAttemptsUrl: type: [string, 'null'] format: uri-reference ReadySectionMetadata: type: object additionalProperties: false required: [calculatedAt, datasetSnapshotId, metricDefinitionVersion] properties: calculatedAt: type: string format: date-time datasetSnapshotId: type: string pattern: '^ds_[A-Za-z0-9_-]{8,60}$' metricDefinitionVersion: type: string const: 0.4.0 IntegerMetric: type: object additionalProperties: false required: [metricId, value, unit] properties: metricId: type: string pattern: '^[a-z0-9_]+$' value: type: integer minimum: 0 unit: type: string enum: [SESSIONS, RIAL] DecimalMetric: type: object additionalProperties: false required: [metricId, value, unit, numerator, denominator] properties: metricId: type: string pattern: '^[a-z0-9_]+$' value: type: [string, 'null'] pattern: '^(0|[1-9][0-9]*)(\.[0-9]{1,6})?$' description: Null only when the denominator is zero or source is unavailable. unit: type: string enum: [PERCENT, RATIO] numerator: type: integer minimum: 0 denominator: type: integer minimum: 0 TimingDistribution: type: object additionalProperties: false required: [metricId, p50Ms, p95Ms, sampleSize, eligibleSampleSize, coveragePercent] properties: metricId: type: string enum: [init_time_ms, verify_time_ms] p50Ms: type: [integer, 'null'] minimum: 0 p95Ms: type: [integer, 'null'] minimum: 0 sampleSize: type: integer minimum: 0 eligibleSampleSize: type: integer minimum: 0 coveragePercent: type: [string, 'null'] pattern: '^(100|[0-9]{1,2})(\.[0-9]{1,6})?$' PreviousPeriodComparison: type: object additionalProperties: false required: [state, baselinePeriod] properties: state: type: string enum: [AVAILABLE, UNAVAILABLE] baselinePeriod: $ref: '#/components/schemas/ReportingPeriod' absoluteChange: type: [string, 'null'] pattern: '^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$' relativeChangePercent: type: [string, 'null'] pattern: '^-?(0|[1-9][0-9]*)(\.[0-9]{1,6})?$' unavailableReason: type: [string, 'null'] enum: [EMPTY_BASELINE, ZERO_BASELINE, SOURCE_UNAVAILABLE, null] isActionableInsightEligible: type: boolean OverviewData: type: object additionalProperties: false required: - bankPaidSessions - bankPaidAmountRial - paymentSuccessRate - verificationCompletionRate - comparisons - paymentHistory properties: bankPaidSessions: allOf: - $ref: '#/components/schemas/IntegerMetric' example: {metricId: bank_paid_sessions, value: 420, unit: SESSIONS} bankPaidAmountRial: allOf: - $ref: '#/components/schemas/IntegerMetric' example: {metricId: bank_paid_amount_rial, value: 680000000, unit: RIAL} paymentSuccessRate: $ref: '#/components/schemas/DecimalMetric' verificationCompletionRate: $ref: '#/components/schemas/DecimalMetric' comparisons: type: object additionalProperties: $ref: '#/components/schemas/PreviousPeriodComparison' propertyNames: enum: [bank_paid_sessions, bank_paid_amount_rial, payment_success_rate, verification_completion_rate] paymentHistory: type: array maxItems: 93 items: $ref: '#/components/schemas/PaymentHistoryPoint' PaymentHistoryPoint: type: object additionalProperties: false required: [localDate, totalSessions, bankPaidSessions, bankPaidAmountRial] properties: localDate: type: string format: date totalSessions: type: integer format: int64 minimum: 0 bankPaidSessions: type: integer format: int64 minimum: 0 bankPaidAmountRial: type: integer format: int64 minimum: 0 PaymentPerformanceData: type: object additionalProperties: false required: - bankPaidSessions - verifiedSessions - unverifiedPaidSessions - failedSessions - reversedSessions - noAttemptSessions - reachedBankSessions - retriedSessions - retryRecoveryRate - initTiming - verifyTiming properties: bankPaidSessions: {$ref: '#/components/schemas/IntegerMetric'} verifiedSessions: {$ref: '#/components/schemas/IntegerMetric'} unverifiedPaidSessions: {$ref: '#/components/schemas/IntegerMetric'} failedSessions: {$ref: '#/components/schemas/IntegerMetric'} reversedSessions: {$ref: '#/components/schemas/IntegerMetric'} noAttemptSessions: {$ref: '#/components/schemas/IntegerMetric'} reachedBankSessions: {$ref: '#/components/schemas/IntegerMetric'} retriedSessions: {$ref: '#/components/schemas/IntegerMetric'} retryRecoveryRate: {$ref: '#/components/schemas/DecimalMetric'} initTiming: {$ref: '#/components/schemas/TimingDistribution'} verifyTiming: {$ref: '#/components/schemas/TimingDistribution'} FeesData: type: object additionalProperties: false required: [paidFeeAmountRial, bankPaidAmountRial, paidFeeRate, comparison] properties: paidFeeAmountRial: {$ref: '#/components/schemas/IntegerMetric'} bankPaidAmountRial: {$ref: '#/components/schemas/IntegerMetric'} paidFeeRate: {$ref: '#/components/schemas/DecimalMetric'} comparison: {$ref: '#/components/schemas/PreviousPeriodComparison'} PeerMetric: type: object additionalProperties: false required: [metricId, cohortMedian, merchantPerformancePercentile, direction] properties: metricId: type: string enum: - payment_success_rate - verification_completion_rate - retry_recovery_rate - init_time_ms_p50 - init_time_ms_p95 - verify_time_ms_p50 - verify_time_ms_p95 - paid_fee_rate cohortMedian: type: string pattern: '^(0|[1-9][0-9]*)(\.[0-9]{1,6})?$' merchantPerformancePercentile: type: string pattern: '^(100|[0-9]{1,2})(\.[0-9]{1,6})?$' description: Higher always means better; lower-is-better metrics are direction-adjusted. direction: type: string enum: [HIGHER_IS_BETTER, LOWER_IS_BETTER] PeerComparisonData: type: object additionalProperties: false required: [eligiblePeerCount, combinedPeerSessionCount, metrics] properties: eligiblePeerCount: type: integer minimum: 10 combinedPeerSessionCount: type: integer minimum: 1000 metrics: type: array minItems: 8 maxItems: 8 items: $ref: '#/components/schemas/PeerMetric' OverviewReadySection: type: object additionalProperties: false required: [sectionId, state, metadata, data] properties: sectionId: {type: string, const: overview} state: {type: string, const: READY} metadata: {$ref: '#/components/schemas/ReadySectionMetadata'} data: {$ref: '#/components/schemas/OverviewData'} PaymentPerformanceReadySection: type: object additionalProperties: false required: [sectionId, state, metadata, data] properties: sectionId: {type: string, const: payment_performance} state: {type: string, const: READY} metadata: {$ref: '#/components/schemas/ReadySectionMetadata'} data: {$ref: '#/components/schemas/PaymentPerformanceData'} FeesReadySection: type: object additionalProperties: false required: [sectionId, state, metadata, data] properties: sectionId: {type: string, const: fees} state: {type: string, const: READY} metadata: {$ref: '#/components/schemas/ReadySectionMetadata'} data: {$ref: '#/components/schemas/FeesData'} PeerComparisonReadySection: type: object additionalProperties: false required: [sectionId, state, metadata, data] properties: sectionId: {type: string, const: peer_comparison} state: {type: string, const: READY} metadata: {$ref: '#/components/schemas/ReadySectionMetadata'} data: {$ref: '#/components/schemas/PeerComparisonData'} PeerComparisonSuppressedSection: type: object additionalProperties: false required: - sectionId - state - metadata - reason - eligiblePeerCount - combinedPeerSessionCount - requiredPeerCount - requiredPeerSessionCount properties: sectionId: {type: string, const: peer_comparison} state: {type: string, const: SUPPRESSED} metadata: {$ref: '#/components/schemas/ReadySectionMetadata'} reason: type: string enum: [INSUFFICIENT_COHORT, TERMINAL_SCOPE_UNSUPPORTED] description: | `TERMINAL_SCOPE_UNSUPPORTED` is returned for terminal-scoped runs because the approved peer cohort is merchant-grain and must not be presented as if it benchmarked one terminal. eligiblePeerCount: type: integer minimum: 0 combinedPeerSessionCount: type: integer minimum: 0 requiredPeerCount: type: integer const: 10 requiredPeerSessionCount: type: integer const: 1000 OverviewSection: oneOf: - $ref: '#/components/schemas/QueuedSection' - $ref: '#/components/schemas/CalculatingSection' - $ref: '#/components/schemas/FailedSection' - $ref: '#/components/schemas/OverviewReadySection' PaymentPerformanceSection: oneOf: - $ref: '#/components/schemas/QueuedSection' - $ref: '#/components/schemas/CalculatingSection' - $ref: '#/components/schemas/FailedSection' - $ref: '#/components/schemas/PaymentPerformanceReadySection' FeesSection: oneOf: - $ref: '#/components/schemas/QueuedSection' - $ref: '#/components/schemas/CalculatingSection' - $ref: '#/components/schemas/FailedSection' - $ref: '#/components/schemas/FeesReadySection' PeerComparisonSection: oneOf: - $ref: '#/components/schemas/QueuedSection' - $ref: '#/components/schemas/CalculatingSection' - $ref: '#/components/schemas/FailedSection' - $ref: '#/components/schemas/PeerComparisonReadySection' - $ref: '#/components/schemas/PeerComparisonSuppressedSection' DetailView: type: string enum: [ALL, UNVERIFIED_PAID, FAILED, RETRIED] FailureStage: type: string enum: [NO_ATTEMPT, REACHED_BANK, OTHER_OBSERVED] SessionStatus: type: string enum: [Paid, Verified, Failed, Reversed] AttemptStatus: type: string enum: [InBank, Paid, Verified, Failed, Reversed] AttemptSummary: type: object additionalProperties: false required: [sequence, status] properties: sequence: type: integer minimum: 1 maximum: 135 status: $ref: '#/components/schemas/AttemptStatus' createdAt: type: [string, 'null'] format: date-time initTimeMs: type: [integer, 'null'] minimum: 0 verifyTimeMs: type: [integer, 'null'] minimum: 0 SessionDetail: type: object additionalProperties: false required: - sessionId - terminalId - sessionStatus - amountRial - feeAmountRial - createdAt - attemptCount - attempts properties: sessionId: type: string description: Opaque merchant-local API id, not the internal database key. minLength: 8 maxLength: 128 terminalId: type: string description: Existing opaque source terminal reference shown as `شناسه پایانه`; tenant-scoped display/trace data only. minLength: 1 maxLength: 128 sessionStatus: $ref: '#/components/schemas/SessionStatus' amountRial: type: integer minimum: 0 feeAmountRial: type: integer minimum: 0 createdAt: type: string format: date-time expiresAt: type: [string, 'null'] format: date-time verifiedAt: type: [string, 'null'] format: date-time settledAt: type: [string, 'null'] format: date-time attemptCount: type: integer minimum: 0 failureStage: oneOf: - $ref: '#/components/schemas/FailureStage' - type: 'null' attempts: type: array maxItems: 135 description: Bounded ordered attempt summary; payer tokens and response codes are absent. items: $ref: '#/components/schemas/AttemptSummary' PageInfo: type: object additionalProperties: false required: [limit, hasNextPage, nextCursor] properties: limit: type: integer minimum: 1 maximum: 100 hasNextPage: type: boolean nextCursor: type: [string, 'null'] minLength: 16 maxLength: 2048 SessionDetailPage: type: object additionalProperties: false required: [period, view, appliedFilters, items, pageInfo] properties: period: $ref: '#/components/schemas/ReportingPeriod' view: $ref: '#/components/schemas/DetailView' appliedFilters: type: object additionalProperties: false properties: terminalId: type: [string, 'null'] failureStage: oneOf: - $ref: '#/components/schemas/FailureStage' - type: 'null' status: oneOf: - $ref: '#/components/schemas/SessionStatus' - type: 'null' minAmountRial: type: [integer, 'null'] format: int64 minimum: 0 maxAmountRial: type: [integer, 'null'] format: int64 minimum: 0 items: type: array maxItems: 100 items: $ref: '#/components/schemas/SessionDetail' pageInfo: $ref: '#/components/schemas/PageInfo' EventId: type: string minLength: 12 maxLength: 128 pattern: '^evt_[A-Za-z0-9]+$' SseEventBase: type: object required: [eventId, runId, occurredAt] properties: eventId: {$ref: '#/components/schemas/EventId'} runId: {$ref: '#/components/schemas/RunId'} occurredAt: {type: string, format: date-time} RunSnapshotEvent: unevaluatedProperties: false allOf: - $ref: '#/components/schemas/SseEventBase' - type: object required: [recovery, run] properties: recovery: type: string enum: [INITIAL, REPLAYED, RESET_TO_SNAPSHOT] run: {$ref: '#/components/schemas/AnalyticsRun'} SectionStartedEvent: unevaluatedProperties: false allOf: - $ref: '#/components/schemas/SseEventBase' - type: object required: [sectionId, startedAt] properties: sectionId: {$ref: '#/components/schemas/SectionId'} startedAt: {type: string, format: date-time} SectionReadyEvent: unevaluatedProperties: false allOf: - $ref: '#/components/schemas/SseEventBase' - type: object required: [sectionId, section] properties: sectionId: {$ref: '#/components/schemas/SectionId'} section: oneOf: - $ref: '#/components/schemas/OverviewReadySection' - $ref: '#/components/schemas/PaymentPerformanceReadySection' - $ref: '#/components/schemas/FeesReadySection' - $ref: '#/components/schemas/PeerComparisonReadySection' SectionFailedEvent: unevaluatedProperties: false allOf: - $ref: '#/components/schemas/SseEventBase' - type: object required: [sectionId, section] properties: sectionId: {$ref: '#/components/schemas/SectionId'} section: {$ref: '#/components/schemas/FailedSection'} SectionSuppressedEvent: unevaluatedProperties: false allOf: - $ref: '#/components/schemas/SseEventBase' - type: object required: [sectionId, section] properties: sectionId: {type: string, const: peer_comparison} section: {$ref: '#/components/schemas/PeerComparisonSuppressedSection'} RunCompletedEvent: unevaluatedProperties: false allOf: - $ref: '#/components/schemas/SseEventBase' - type: object required: [run] properties: run: {$ref: '#/components/schemas/AnalyticsRun'} HeartbeatEvent: unevaluatedProperties: false allOf: - $ref: '#/components/schemas/SseEventBase' - type: object required: [serverTime] properties: serverTime: {type: string, format: date-time} AnalyticsSseEvent: description: JSON payload carried in the SSE `data` field; event name selects the schema. oneOf: - $ref: '#/components/schemas/RunSnapshotEvent' - $ref: '#/components/schemas/SectionStartedEvent' - $ref: '#/components/schemas/SectionReadyEvent' - $ref: '#/components/schemas/SectionFailedEvent' - $ref: '#/components/schemas/SectionSuppressedEvent' - $ref: '#/components/schemas/RunCompletedEvent' - $ref: '#/components/schemas/HeartbeatEvent' FieldError: type: object additionalProperties: false required: [field, reason] properties: field: type: string minLength: 1 maxLength: 100 description: Allowlisted public field name; never a database path. reason: type: string minLength: 3 maxLength: 100 pattern: '^[A-Z0-9_]+$' Problem: type: object description: RFC 9457 Problem Details with stable application extensions. additionalProperties: false required: [type, title, status, detail, instance, code, requestId] properties: type: type: string format: uri description: Stable problem type URI under `https://zarinpal.amiirkhl.ir/docs/errors/{error-slug}`. title: type: string minLength: 1 maxLength: 200 status: type: integer minimum: 400 maximum: 599 description: Must equal the actual HTTP response status. detail: type: string minLength: 1 maxLength: 1000 description: Safe occurrence-specific guidance; clients must not parse it. instance: type: string format: uri-reference code: type: string minLength: 3 maxLength: 100 pattern: '^[A-Z0-9_]+$' description: Stable application error code used by clients. requestId: type: string minLength: 8 maxLength: 128 errors: type: array maxItems: 50 items: $ref: '#/components/schemas/FieldError' snapshotUrl: type: string format: uri-reference description: Optional safe recovery link for run-related problems. nextRefreshAt: type: string format: date-time description: Present for REFRESH_LIMITED responses.