2142 lines
74 KiB
YAML
2142 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.
|
|
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, 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}
|
|
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.
|