Files
zarinpal-analytics-backend/docs/openapi.yaml

2150 lines
74 KiB
YAML

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=<opaque>; 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
localHour:
type: integer
minimum: 0
maximum: 23
description: Tehran-local hour bucket, present only for DAY periods.
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.