openapi: 3.0.3
info:
title: 'Emza Platform — Public API v1'
description: 'Public REST API for Contract creation and Electronic Signature lifecycle. Bilingual (fa/en) errors. RFC 7807 problem+json envelope.'
version: 1.0.0
servers:
-
url: 'https://elemza.com'
tags:
-
name: Contracts
description: ''
-
name: 'Digital certificates (PKI)'
description: "\nIssuing a certificate is not something an API can do on somebody's behalf: the law wants an OTP\nthat person typed and a live face check they attended. So these endpoints do the part an API\nlegitimately can — start the enrolment, hand back a link for the person, and let the integrator\nfollow it to the end."
-
name: 'Form submissions — fill a form through the API (ADR-0029)'
description: "\nSubmit-once semantics: no draft/resume, no edit-after-submit (FB SCOPE-CONTRACT rows 6/7).\nOwnership: every endpoint requires the form to BELONG to the token owner (v2 schema only) —\nthe integrator drives fills of their OWN forms; end users never hold tokens."
-
name: 'Forms — read-only access to v2 forms owned by the authenticated user.'
description: "\nPhase 5 MVP: list + show + stats. Submissions endpoint will land in Phase 5b\nwith proper pagination + transformer."
-
name: 'Meta — health, identity, quota, cost'
description: ''
-
name: Signers
description: "\nSigner endpoints for a contract.\n\nPhase 1 = read-only (index + show). Resend + delete = Phase 2 (mutations).\n\nSpec: docs/api-master-plan/02-endpoint-surface.md (Signer endpoints)."
-
name: Templates
description: "\nTemplate endpoints — read-only in v1.0. CRUD lands in v1.x.\n\nSpec: docs/api-master-plan/02-endpoint-surface.md (Template endpoints)."
-
name: Webhooks
description: "\nOutbound webhook subscriptions. Customers create one or more endpoints, each\nfiltered by an array of event names. Deliveries are signed with HMAC-SHA256\nvia the secret returned at create-time (shown only once).\n\nScope: `webhooks:manage`."
components:
securitySchemes:
default:
type: http
scheme: bearer
description: 'Create your token in the dashboard at /dashboard/api-tokens. The plain token is shown once at creation. Format is {id}|{secret} — send it verbatim in the Authorization header, no pat_live_ prefix.'
security:
-
default: []
paths:
'/api/v1/contracts/{code}/dl':
get:
summary: 'Public signed-URL download.'
operationId: publicSignedURLDownload
description: "Validates Laravel's `signed` middleware. No Bearer token required —\nthe URL signature is the auth proof. The signature is generated by\n`downloadPdf()` after the bearer was verified, so this is functionally\nequivalent to a 24h time-bounded capability URL.\n\nRecords an `ApiRequestLog`-style entry via the underlying `ContractDownload`\naudit table (same trail as the user-panel signed-link download flow)."
parameters: []
responses:
404:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/not_found'
status: 404
code: not_found
title_fa: 'منبع یافت نشد'
title_en: 'Resource not found'
request_id: 01M0TB569B06B153MHVXBVGBW2
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/not_found'
status:
type: integer
example: 404
code:
type: string
example: not_found
title_fa:
type: string
example: 'منبع یافت نشد'
title_en:
type: string
example: 'Resource not found'
request_id:
type: string
example: 01M0TB569B06B153MHVXBVGBW2
tags:
- Contracts
parameters:
-
in: path
name: code
description: '16-char public code.'
example: architecto
required: true
schema:
type: string
/api/v1/contracts:
get:
summary: 'List contracts'
operationId: listContracts
description: "Returns paginated contracts visible to caller. Org root sees own + descendants'\nnon-private contracts; non-root sees own + contracts where they're a signer.\n\nScope: `contracts:read`.\n\n**PII filter scope requirement:** filtering by `signer_mobile` or\n`signer_national_code` requires the additional `signers:read_pii` ability\non your token. Without it, those query parameters return\n**403 `scope_missing`**. This prevents a low-privilege `contracts:read`\ntoken from using the index as a confirmation oracle to check whether a\ngiven mobile/national-code signs any contract on the owner's tree.\n\nThe `*` wildcard scope also passes the check."
parameters:
-
in: query
name: status
description: 'Filter by status: draft|waiting_signature|completed|voided|canceled.'
example: waiting_signature
required: false
schema:
type: string
description: 'Filter by status: draft|waiting_signature|completed|voided|canceled.'
example: waiting_signature
-
in: query
name: signature_method
description: 'Filter by signing method: electronic|digital.'
example: digital
required: false
schema:
type: string
description: 'Filter by signing method: electronic|digital.'
example: digital
-
in: query
name: created_after
description: 'ISO 8601 datetime.'
example: '2026-01-01T00:00:00Z'
required: false
schema:
type: string
description: 'ISO 8601 datetime.'
example: '2026-01-01T00:00:00Z'
-
in: query
name: created_before
description: 'ISO 8601 datetime.'
example: architecto
required: false
schema:
type: string
description: 'ISO 8601 datetime.'
example: architecto
-
in: query
name: signer_mobile
description: 'Iran mobile regex 09XXXXXXXXX. **Requires `signers:read_pii` scope.**'
example: '09121234567'
required: false
schema:
type: string
description: 'Iran mobile regex 09XXXXXXXXX. **Requires `signers:read_pii` scope.**'
example: '09121234567'
-
in: query
name: signer_national_code
description: '10-digit Iran national code. **Requires `signers:read_pii` scope.**'
example: architecto
required: false
schema:
type: string
description: '10-digit Iran national code. **Requires `signers:read_pii` scope.**'
example: architecto
-
in: query
name: template_id
description: 'Filter by template.'
example: 16
required: false
schema:
type: integer
description: 'Filter by template.'
example: 16
-
in: query
name: is_private
description: 'Filter by privacy flag.'
example: false
required: false
schema:
type: boolean
description: 'Filter by privacy flag.'
example: false
-
in: query
name: page
description: 'Default 1.'
example: 16
required: false
schema:
type: integer
description: 'Default 1.'
example: 16
-
in: query
name: per_page
description: 'Max 100. Default 20.'
example: 16
required: false
schema:
type: integer
description: 'Max 100. Default 20.'
example: 16
-
in: query
name: sort
description: 'created_at | -created_at | completed_at | -completed_at. Default -created_at.'
example: architecto
required: false
schema:
type: string
description: 'created_at | -created_at | completed_at | -completed_at. Default -created_at.'
example: architecto
responses:
401:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/token_invalid'
status: 401
code: token_invalid
title_fa: 'توکن نامعتبر است'
title_en: 'Invalid token'
request_id: 01M0TB56ECVSX0CKVWKG3R4TNJ
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/token_invalid'
status:
type: integer
example: 401
code:
type: string
example: token_invalid
title_fa:
type: string
example: 'توکن نامعتبر است'
title_en:
type: string
example: 'Invalid token'
request_id:
type: string
example: 01M0TB56ECVSX0CKVWKG3R4TNJ
403:
description: 'PII filter without scope'
content:
application/json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/scope_missing'
status: 403
code: scope_missing
title_fa: 'این فیلتر نیاز به مجوز signers:read_pii دارد'
title_en: 'This filter requires the signers:read_pii scope'
request_id: 01KRH8JRC4Y855P10CYC1C0AYS
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/scope_missing'
status:
type: integer
example: 403
code:
type: string
example: scope_missing
title_fa:
type: string
example: 'این فیلتر نیاز به مجوز signers:read_pii دارد'
title_en:
type: string
example: 'This filter requires the signers:read_pii scope'
request_id:
type: string
example: 01KRH8JRC4Y855P10CYC1C0AYS
tags:
- Contracts
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
status:
type: string
description: ''
example: waiting_signature
enum:
- draft
- waiting_signature
- completed
- voided
- canceled
nullable: true
signature_method:
type: string
description: ''
example: digital
enum:
- electronic
- digital
nullable: true
created_after:
type: string
description: 'value یک تاریخ معتبر نیست.'
example: '2026-08-24T20:25:15'
nullable: true
created_before:
type: string
description: 'value یک تاریخ معتبر نیست.'
example: '2026-08-24T20:25:15'
nullable: true
signer_mobile:
type: string
description: 'Must match the regex /^09[0-9]{9}$/.'
example: '09564255931'
nullable: true
signer_national_code:
type: string
description: 'value باید 10 کاراکتر باشد.'
example: ikhwaykcmy
nullable: true
template_id:
type: integer
description: ''
example: 16
nullable: true
is_private:
type: boolean
description: ''
example: true
nullable: true
page:
type: integer
description: 'value باید حداقل 1 باشد.'
example: 22
nullable: true
per_page:
type: integer
description: 'value باید حداقل 1 باشد. value نباید بیشتر از 100 باشد.'
example: 7
nullable: true
sort:
type: string
description: ''
example: created_at
enum:
- created_at
- '-created_at'
- completed_at
- '-completed_at'
nullable: true
post:
summary: 'Create contract'
operationId: createContract
description: "Creates a new contract. Two modes:\n\n**One-shot** — multipart/form-data with `file` field + metadata. File is\npersisted to staging and `ProcessContractFilesJob` is dispatched. Optional\n`signers[]` are added by the same job after page rasterization completes.\nResponse is **202 Accepted**; poll `_links.processing` until `ready=true`.\n\n**Multi-step** — JSON without `file`. Creates a `draft` contract row only.\nCaller then issues `POST /contracts/{code}/files` to attach the document.\n\n### Signature placement — `template_id` + `template_enforcement`\n\nPassing `template_id` snapshots that template's signature slots onto the new\ncontract. `template_enforcement` decides what those slots MEAN to the signer:\n\n- `locked` — each slot is pinned: the signer cannot drag the signature off it,\n and cannot sign on a page the template never assigned them.\n **This is the default whenever `template_id` is present.**\n- `suggested` — the signature merely *starts* on the slot; the signer may move\n it anywhere, on any page.\n\n`strict` / `loose` are accepted as legacy aliases of `locked` / `suggested`\n(the spelling earlier revisions of this page advertised).\n\nIf the template pins slots on more than one page, send `all_pages_signature=true`\nas well — otherwise one signature on any allowed page completes the contract.\n\nVerify what was applied on the create response (`template_id`,\n`template_enforcement`) or in full via `GET /contracts/{code}` → `template_positions`.\n\nScope: `contracts:write`."
parameters: []
responses:
422:
description: 'template_id not reachable by the caller'
content:
application/json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/template_not_accessible'
status: 422
code: template_not_accessible
title_fa: 'قالب انتخابشده وجود ندارد یا در دسترس شما نیست'
title_en: 'The selected template does not exist or is not accessible to you'
request_id: 01KRH8JRC4Y855P10CYC1C0AYS
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/template_not_accessible'
status:
type: integer
example: 422
code:
type: string
example: template_not_accessible
title_fa:
type: string
example: 'قالب انتخابشده وجود ندارد یا در دسترس شما نیست'
title_en:
type: string
example: 'The selected template does not exist or is not accessible to you'
request_id:
type: string
example: 01KRH8JRC4Y855P10CYC1C0AYS
tags:
- Contracts
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
title:
type: string
description: 'Contract title (1..120 chars).'
example: 'قرارداد بیمه شخص ثالث'
signature_method:
type: string
description: "electronic|digital. Default electronic. `digital` = PADES\n signature via the Pendar CA cloud-HSM. Requires the PKI feature to be enabled for\n your account (pre-flight via `GET /me` → `data.capabilities.pki`), otherwise\n **403 `pki_not_available`**. The method is contract-wide (all signers sign the same\n way) and IMMUTABLE after creation — to change it, cancel and recreate (ADR-0009)."
example: architecto
all_pages_signature:
type: boolean
description: 'Default false. When true, every page must carry the signature element.'
example: false
is_private:
type: boolean
description: 'Default false. When true, hides contract from org-ancestors.'
example: false
template_id:
type: integer
description: "Optional — must be owned by the caller or shared from the org root;\n anything else is rejected with 422 `template_not_accessible` (never silently dropped).\n List valid ids via `GET /api/v1/templates`."
example: 26
template_enforcement:
type: string
description: "locked|suggested (legacy aliases: strict|loose).\n Defaults to `locked` when `template_id` is set."
example: locked
redirect_url:
type: string
description: "HTTPS URL to send the signer back to after they finish signing.\n They see our confirmation first, then return automatically. Without it they stay on our\n confirmation page."
example: 'https://partner.example/contracts/42/signed'
file:
type: string
format: binary
description: 'PDF or PNG/JPG, max 4MB (multipart/form-data only).'
source:
type: object
description: "Optional document source instead of `file`. Currently only\n `{type: \"form_submission\", code: \"ABCD1234EFGH5678\"}` — uses the rendered PDF of one\n of YOUR form submissions as this contract's document (the فرم → PDF → امضا chain,\n works with both signature methods). 409 `not_ready` if the submission PDF is still\n rendering; 422 `not_applicable` for online-renderer forms (no PDF artifact)."
example: []
properties: { }
signers:
type: array
description: 'Optional inline signers (one-shot). Same shape as POST /contracts/{code}/signers.'
example:
- architecto
items:
type: string
required:
- title
'/api/v1/contracts/{code}':
get:
summary: 'Get contract'
operationId: getContract
description: "Returns full contract shape with embedded signers, per-page geometry,\nand template positions snapshot.\n\nScope: `contracts:read`."
parameters: []
responses:
401:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/token_invalid'
status: 401
code: token_invalid
title_fa: 'توکن نامعتبر است'
title_en: 'Invalid token'
request_id: 01M0TB56EQYEJPZ984WW270GM5
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/token_invalid'
status:
type: integer
example: 401
code:
type: string
example: token_invalid
title_fa:
type: string
example: 'توکن نامعتبر است'
title_en:
type: string
example: 'Invalid token'
request_id:
type: string
example: 01M0TB56EQYEJPZ984WW270GM5
tags:
- Contracts
delete:
summary: 'Cancel contract'
operationId: cancelContract
description: "Cancels a contract in `draft` or `waiting_signature` state. Refunds the\nbilled amount to the original wallet (100% if no signer signed, 50% if any\nShahkar was consumed). Deletes contract files from storage. Waiting signers\nreceive an SMS notifying them the contract was canceled.\n\nScope: `contracts:write`."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data:
code: ABC1234567XYZ890
status: canceled
canceled_at: '2026-05-11T10:00:00.000Z'
refund:
amount: 60000
currency: IRT
properties:
data:
type: object
properties:
code:
type: string
example: ABC1234567XYZ890
status:
type: string
example: canceled
canceled_at:
type: string
example: '2026-05-11T10:00:00.000Z'
refund:
type: object
properties:
amount:
type: integer
example: 60000
currency:
type: string
example: IRT
tags:
- Contracts
parameters:
-
in: path
name: code
description: '16-char public code (A-Z, 0-9).'
example: ABC1234567XYZ890
required: true
schema:
type: string
'/api/v1/contracts/{code}/pdf':
get:
summary: 'Download signed PDF'
operationId: downloadSignedPDF
description: "Returns the final signed PDF in one of three formats (Phase 3):\n\n- `format=redirect` (default) — `302 Found` to a signed URL (24h TTL).\n The signed URL points at the unauthenticated `/api/v1/contracts/{code}/dl`\n route, which validates the signature and streams the file. Use this when\n embedding in `` for partner-facing UIs.\n- `format=url` — JSON envelope with `url`, `expires_at`, `filename`, `size_bytes`,\n `checksum_sha256`. Use this when the API client wants to hand the URL\n to another system, embed in email, or pre-flight before downloading.\n- `format=stream` — streams the PDF inline (consumes API egress). Use when\n the caller is a backend service that wants the bytes in one round-trip.\n\nScope: `contracts:read`."
parameters:
-
in: query
name: format
description: 'redirect|url|stream. Default redirect.'
example: url
required: false
schema:
type: string
description: 'redirect|url|stream. Default redirect.'
example: url
-
in: query
name: ttl
description: 'Signed URL lifetime in seconds (60..86400). Default 86400. Ignored when `format=stream`.'
example: 16
required: false
schema:
type: integer
description: 'Signed URL lifetime in seconds (60..86400). Default 86400. Ignored when `format=stream`.'
example: 16
responses:
404:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/not_found'
status: 404
code: not_found
title_fa: 'منبع یافت نشد'
title_en: 'Resource not found'
request_id: 01M0TB56F7FRJNS78ATMT67JVA
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/not_found'
status:
type: integer
example: 404
code:
type: string
example: not_found
title_fa:
type: string
example: 'منبع یافت نشد'
title_en:
type: string
example: 'Resource not found'
request_id:
type: string
example: 01M0TB56F7FRJNS78ATMT67JVA
tags:
- Contracts
parameters:
-
in: path
name: code
description: '16-char public code.'
example: architecto
required: true
schema:
type: string
'/api/v1/contracts/{code}/processing':
get:
summary: 'Poll processing status'
operationId: pollProcessingStatus
description: "Lightweight (<200 bytes) status of the rasterization + signer-creation job.\nReads `contract:progress:{id}` cache key directly — no DB hit.\n\n**Step values:** `files`, `signers`, `sms`, `done`, `error`.\n\nReturns `ready=true` when `step=done`. If progress key is gone (TTL expired)\nand contract has signers + pages, falls back to `ready=true` (job finished\nearlier and key was reaped).\n\nScope: `contracts:read`."
parameters: []
responses:
404:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/not_found'
status: 404
code: not_found
title_fa: 'منبع یافت نشد'
title_en: 'Resource not found'
request_id: 01M0TB56FMBZKHZN0ZY16GMJAC
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/not_found'
status:
type: integer
example: 404
code:
type: string
example: not_found
title_fa:
type: string
example: 'منبع یافت نشد'
title_en:
type: string
example: 'Resource not found'
request_id:
type: string
example: 01M0TB56FMBZKHZN0ZY16GMJAC
tags:
- Contracts
parameters:
-
in: path
name: code
description: '16-char contract code.'
example: architecto
required: true
schema:
type: string
'/api/v1/contracts/{code}/files':
post:
summary: 'Upload contract file(s) (two-step flow)'
operationId: uploadContractFilestwoStepFlow
description: "Attaches a file to an existing **draft** contract that has no pages yet.\nSame async pipeline as `POST /contracts` one-shot: stages bytes, dispatches\n`ProcessContractFilesJob`, returns 202 with the `processing` link.\n\nScope: `contracts:write`."
parameters: []
responses: { }
tags:
- Contracts
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
description: 'PDF or PNG/JPG, max 4MB (multipart/form-data).'
required:
- file
parameters:
-
in: path
name: code
description: '16-char contract code.'
example: architecto
required: true
schema:
type: string
/api/v1/certificates:
get:
summary: 'Certificate status for one of your people'
operationId: certificateStatusForOneOfYourPeople
description: "Answers \"can this person sign digitally yet?\" for somebody you have enrolled.\n\n**Privacy** — this is not a lookup service for arbitrary national codes. It only answers for\npeople you yourself started an enrolment for; anyone else returns 404, so the endpoint cannot\nbe used to discover whether a given Iranian holds a certificate.\n\nScope required: `certificates:read`."
parameters:
-
in: query
name: national_code
description: "The person's national code."
example: '0010350829'
required: true
schema:
type: string
description: "The person's national code."
example: '0010350829'
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Active certificate'
type: object
example:
data:
national_code: '0010350829'
status: active
valid_from: '2026-07-30T09:12:00.000Z'
valid_until: '2027-07-30T09:12:00.000Z'
mode: live
properties:
data:
type: object
properties:
national_code:
type: string
example: '0010350829'
status:
type: string
example: active
valid_from:
type: string
example: '2026-07-30T09:12:00.000Z'
valid_until:
type: string
example: '2027-07-30T09:12:00.000Z'
mode:
type: string
example: live
-
description: 'Enrolment still in progress'
type: object
example:
data:
national_code: '0010350829'
status: pending_ekyc
valid_from: null
valid_until: null
mode: live
properties:
data:
type: object
properties:
national_code:
type: string
example: '0010350829'
status:
type: string
example: pending_ekyc
valid_from:
type: string
example: null
nullable: true
valid_until:
type: string
example: null
nullable: true
mode:
type: string
example: live
tags:
- 'Digital certificates (PKI)'
'/api/v1/certificates/{id}':
get:
summary: 'One enrolment request'
operationId: oneEnrolmentRequest
description: "The state of a single enrolment you started, by the `request_id` you received.\n\nScope required: `certificates:read`."
parameters: []
responses:
401:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/token_invalid'
status: 401
code: token_invalid
title_fa: 'توکن نامعتبر است'
title_en: 'Invalid token'
request_id: 01M0TB56P8HCD9VKZECE1SNVS5
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/token_invalid'
status:
type: integer
example: 401
code:
type: string
example: token_invalid
title_fa:
type: string
example: 'توکن نامعتبر است'
title_en:
type: string
example: 'Invalid token'
request_id:
type: string
example: 01M0TB56P8HCD9VKZECE1SNVS5
tags:
- 'Digital certificates (PKI)'
parameters:
-
in: path
name: id
description: 'The `request_id` from the onboarding call.'
example: 01jqk8x2m4n6p8r0s2t4v6w8y0
required: true
schema:
type: string
/api/v1/certificates/onboarding:
post:
summary: 'Start a certificate enrolment'
operationId: startACertificateEnrolment
description: "Creates a short-lived, single-use link for one of your users to obtain a digital certificate.\nSend them to `onboarding_url`; they complete the SMS code, the identity form and the video\ncheck on our pages, and you learn the outcome from `certificate.issued` / `certificate.failed`\n(or by polling `GET /api/v1/certificates/{request_id}`).\n\n**Who pays** — `pay_by: \"issuer\"` charges YOUR wallet for the one-time issuance fee right now\nand refunds it automatically if no certificate is ever issued. `pay_by: \"subject\"` (default)\nlets the person pay on the page.\n\n**Idempotency** — while an enrolment for the same national code is still open you get the SAME\nlink back, not a second one. Pendar treats an open enrolment as a single order.\n\nScope required: `certificates:write`."
parameters: []
responses:
201:
description: 'Link created'
content:
application/json:
schema:
type: object
example:
data:
request_id: 01jqk8x2m4n6p8r0s2t4v6w8y0
onboarding_url: 'https://elemza.com/pki/onboarding/01jqk8x2m4n6p8r0s2t4v6w8y0?expires=...&signature=...'
expires_at: '2026-07-30T12:00:00.000Z'
status: pending
pay_by: issuer
mode: live
properties:
data:
type: object
properties:
request_id:
type: string
example: 01jqk8x2m4n6p8r0s2t4v6w8y0
onboarding_url:
type: string
example: 'https://elemza.com/pki/onboarding/01jqk8x2m4n6p8r0s2t4v6w8y0?expires=...&signature=...'
expires_at:
type: string
example: '2026-07-30T12:00:00.000Z'
status:
type: string
example: pending
pay_by:
type: string
example: issuer
mode:
type: string
example: live
tags:
- 'Digital certificates (PKI)'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
national_code:
type: string
description: "The person's 10-digit national code."
example: '0010350829'
mobile:
type: string
description: 'Their mobile, 09xxxxxxxxx.'
example: '09121234567'
first_name:
type: string
description: 'Optional, prefills the form.'
example: علی
last_name:
type: string
description: 'Optional, prefills the form.'
example: رضایی
pay_by:
type: string
description: '`issuer` or `subject` (default).'
example: issuer
redirect_url:
type: string
description: 'HTTPS URL to send the person back to when they finish.'
example: 'https://partner.example/done'
ttl_minutes:
type: integer
description: 'Link lifetime, 5–1440 (default 60).'
example: 120
required:
- national_code
- mobile
'/api/v1/form-submissions/{code}/dl':
get:
summary: "Public signed-URL download twin (no Bearer — the URL signature is the auth proof,\nminted by downloadPdf() after the bearer was verified). Mirrors contracts /dl."
operationId: publicSignedURLDownloadTwinnoBearerTheURLSignatureIsTheAuthProofMintedByDownloadPdfAfterTheBearerWasVerifiedMirrorsContractsdl
description: ''
parameters: []
responses:
404:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/not_found'
status: 404
code: not_found
title_fa: 'منبع یافت نشد'
title_en: 'Resource not found'
request_id: 01M0TB56B2TW4QKAHSPA045ACY
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/not_found'
status:
type: integer
example: 404
code:
type: string
example: not_found
title_fa:
type: string
example: 'منبع یافت نشد'
title_en:
type: string
example: 'Resource not found'
request_id:
type: string
example: 01M0TB56B2TW4QKAHSPA045ACY
tags:
- 'Form submissions — fill a form through the API (ADR-0029)'
parameters:
-
in: path
name: code
description: '16-char submission code.'
example: architecto
required: true
schema:
type: string
'/api/v1/form-submissions/{code}/files/{field}/dl':
get:
summary: "Public signed-URL twin for a single field file (no Bearer — the signature is the auth\nproof, minted by downloadFieldFile() only after the bearer was verified)."
operationId: publicSignedURLTwinForASingleFieldFilenoBearerTheSignatureIsTheAuthProofMintedByDownloadFieldFileOnlyAfterTheBearerWasVerified
description: ''
parameters: []
responses:
404:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/not_found'
status: 404
code: not_found
title_fa: 'منبع یافت نشد'
title_en: 'Resource not found'
request_id: 01M0TB56BDPVRXEKC0F5PRAYFA
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/not_found'
status:
type: integer
example: 404
code:
type: string
example: not_found
title_fa:
type: string
example: 'منبع یافت نشد'
title_en:
type: string
example: 'Resource not found'
request_id:
type: string
example: 01M0TB56BDPVRXEKC0F5PRAYFA
tags:
- 'Form submissions — fill a form through the API (ADR-0029)'
parameters:
-
in: path
name: code
description: '16-char submission code.'
example: architecto
required: true
schema:
type: string
-
in: path
name: field
description: "The field's technical `name`."
example: architecto
required: true
schema:
type: string
'/api/v1/forms/{slug}/submissions':
get:
summary: 'List submissions'
operationId: listSubmissions
description: 'Paginated, newest first. Scope: `forms:read`.'
parameters:
-
in: query
name: status
description: draft|pending_payment|submitted|processing|completed|signed.
example: architecto
required: false
schema:
type: string
description: draft|pending_payment|submitted|processing|completed|signed.
example: architecto
-
in: query
name: submitted_after
description: 'ISO 8601.'
example: architecto
required: false
schema:
type: string
description: 'ISO 8601.'
example: architecto
-
in: query
name: submitted_before
description: 'ISO 8601.'
example: architecto
required: false
schema:
type: string
description: 'ISO 8601.'
example: architecto
-
in: query
name: has_contract
description: 'Only submissions that bridged to a contract.'
example: false
required: false
schema:
type: boolean
description: 'Only submissions that bridged to a contract.'
example: false
-
in: query
name: per_page
description: 'Max 100, default 20.'
example: 16
required: false
schema:
type: integer
description: 'Max 100, default 20.'
example: 16
responses:
401:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/token_invalid'
status: 401
code: token_invalid
title_fa: 'توکن نامعتبر است'
title_en: 'Invalid token'
request_id: 01M0TB56K1MBR6KBMRYXPDZBE3
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/token_invalid'
status:
type: integer
example: 401
code:
type: string
example: token_invalid
title_fa:
type: string
example: 'توکن نامعتبر است'
title_en:
type: string
example: 'Invalid token'
request_id:
type: string
example: 01M0TB56K1MBR6KBMRYXPDZBE3
tags:
- 'Form submissions — fill a form through the API (ADR-0029)'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
status:
type: string
description: ''
example: submitted
enum:
- draft
- pending_payment
- submitted
- processing
- completed
- signed
nullable: true
submitted_after:
type: string
description: 'value یک تاریخ معتبر نیست.'
example: '2026-08-24T20:25:15'
nullable: true
submitted_before:
type: string
description: 'value یک تاریخ معتبر نیست.'
example: '2026-08-24T20:25:15'
nullable: true
has_contract:
type: boolean
description: ''
example: true
nullable: true
page:
type: integer
description: 'value باید حداقل 1 باشد.'
example: 16
nullable: true
per_page:
type: integer
description: 'value باید حداقل 1 باشد. value نباید بیشتر از 100 باشد.'
example: 22
nullable: true
post:
summary: 'Submit a form (fill via API)'
operationId: submitAFormfillViaAPI
description: "Runs the EXACT same pipeline as the public web renderer: schema validation,\nvisibility filtering, formula computation, trust scoring, the signature bridge\n(`requires_signature` forms auto-create a contract — response embeds it with per-signer\n`signing_url`s), image-overlay PDF rendering (queued), and owner notifications.\n\nScope: `forms:write` + `Idempotency-Key` supported.\n\nFields hidden by visibility conditions are stripped server-side. `query_shahkar` /\n`query_legal_entity` field values must come from `POST /forms/{slug}/verify-field`\n(same request IP) or their matched=true claim is stripped as a forgery.\n\n`file` / `image` fields take EITHER of two shapes, both real:\n - the `path` returned by `POST /forms/{slug}/uploads` — preferred, stores the bytes on\n disk exactly as the web fill flow does, and is readable back via the `/files/{field}`\n endpoint;\n - an inline `data:;base64,…` URI, which is also what a drawn `signature` is.\nBoth are magic-byte validated and size-capped server-side. (This docblock claimed\nuploads were \"not supported in v1 (mirrors the web fill flow)\" until 2026-08-14 — the\nsecond half had been untrue since 2026-06-22, when the web renderer got real uploads,\nand the first half was never enforced by any code: base64 always passed.)"
parameters: []
responses: { }
tags:
- 'Form submissions — fill a form through the API (ADR-0029)'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
data:
type: object
description: 'Map of field name → value.'
example:
first_name: علی
properties: { }
required:
- data
parameters:
-
in: path
name: slug
description: "The form's slug."
example: architecto
required: true
schema:
type: string
'/api/v1/forms/{slug}/submissions/{code}':
get:
summary: 'Get one submission'
operationId: getOneSubmission
description: 'Scope: `forms:read`.'
parameters: []
responses:
404:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/not_found'
status: 404
code: not_found
title_fa: 'منبع یافت نشد'
title_en: 'Resource not found'
request_id: 01M0TB56KHGP7765916R7C299G
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/not_found'
status:
type: integer
example: 404
code:
type: string
example: not_found
title_fa:
type: string
example: 'منبع یافت نشد'
title_en:
type: string
example: 'Resource not found'
request_id:
type: string
example: 01M0TB56KHGP7765916R7C299G
tags:
- 'Form submissions — fill a form through the API (ADR-0029)'
parameters:
-
in: path
name: slug
description: "The form's slug."
example: architecto
required: true
schema:
type: string
-
in: path
name: code
description: '16-char submission code.'
example: architecto
required: true
schema:
type: string
'/api/v1/forms/{slug}/submissions/{code}/pdf':
get:
summary: 'Download submission PDF'
operationId: downloadSubmissionPDF
description: "Image-overlay forms render a final PDF per submission (queued at submit). Same\nsigned-URL family as the contracts PDF endpoint:\n\n- `format=redirect` (default) — 302 to a signed URL (anon-safe, forwardable)\n- `format=url` — JSON envelope with url/expiry/checksum\n- `format=stream` — bytes in this response\n\nScope: `forms:read`."
parameters:
-
in: query
name: format
description: 'redirect|url|stream. Default redirect.'
example: architecto
required: false
schema:
type: string
description: 'redirect|url|stream. Default redirect.'
example: architecto
-
in: query
name: ttl
description: 'Signed URL lifetime seconds (60..86400). Default 86400.'
example: 16
required: false
schema:
type: integer
description: 'Signed URL lifetime seconds (60..86400). Default 86400.'
example: 16
responses:
404:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/not_found'
status: 404
code: not_found
title_fa: 'منبع یافت نشد'
title_en: 'Resource not found'
request_id: 01M0TB56M1X2AGEBJVV6DPVMAM
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/not_found'
status:
type: integer
example: 404
code:
type: string
example: not_found
title_fa:
type: string
example: 'منبع یافت نشد'
title_en:
type: string
example: 'Resource not found'
request_id:
type: string
example: 01M0TB56M1X2AGEBJVV6DPVMAM
tags:
- 'Form submissions — fill a form through the API (ADR-0029)'
parameters:
-
in: path
name: slug
description: "The form's slug."
example: architecto
required: true
schema:
type: string
-
in: path
name: code
description: '16-char submission code.'
example: architecto
required: true
schema:
type: string
'/api/v1/forms/{slug}/submissions/{code}/files/{field}':
get:
summary: 'Download one uploaded field file'
operationId: downloadOneUploadedFieldFile
description: "Serves whatever the submission actually holds for `{field}`: a stored `forms/…` path,\nOR an inline `data:…;base64,…` URI. Both shapes are real and both are served — the\ndata-URI is what a drawn signature is, and what API callers necessarily wrote before\nan uploads endpoint existed, so refusing it would orphan real historical data.\n\nThe field's type is resolved from the submission's OWN schema snapshot, not the live\nform: editing a form must never change what an already-recorded submission is\nunderstood to contain.\n\nSame three formats as the submission PDF endpoint.\n\nScope: `forms:read`."
parameters:
-
in: query
name: format
description: 'redirect|url|stream. Default redirect.'
example: architecto
required: false
schema:
type: string
description: 'redirect|url|stream. Default redirect.'
example: architecto
-
in: query
name: ttl
description: 'Signed URL lifetime seconds (60..86400). Default 86400.'
example: 16
required: false
schema:
type: integer
description: 'Signed URL lifetime seconds (60..86400). Default 86400.'
example: 16
responses:
404:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/not_found'
status: 404
code: not_found
title_fa: 'منبع یافت نشد'
title_en: 'Resource not found'
request_id: 01M0TB56ME91KZHB7W3SNX629B
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/not_found'
status:
type: integer
example: 404
code:
type: string
example: not_found
title_fa:
type: string
example: 'منبع یافت نشد'
title_en:
type: string
example: 'Resource not found'
request_id:
type: string
example: 01M0TB56ME91KZHB7W3SNX629B
tags:
- 'Form submissions — fill a form through the API (ADR-0029)'
parameters:
-
in: path
name: slug
description: "The form's slug."
example: architecto
required: true
schema:
type: string
-
in: path
name: code
description: '16-char submission code.'
example: architecto
required: true
schema:
type: string
-
in: path
name: field
description: "The field's technical `name`."
example: architecto
required: true
schema:
type: string
'/api/v1/forms/{slug}/uploads':
post:
summary: 'Upload a file for a `file` / `image` field'
operationId: uploadAFileForAfileimageField
description: "Multipart, ONE file per call. The returned `path` is what you then put in\n`data.{field}` on `POST /forms/{slug}/submissions`. The bytes land in the form's own\nfolder byte-identically to what the web fill flow produces, so the submissions table,\nthe answer sheet and the image-overlay PDF all render it the same way — an API fill\nand a browser fill become indistinguishable downstream, which is the whole point.\n\nSize and type come from the FIELD's own config (`max_size_kb` / `allowed_mimes` as the\ndesigner set them), clamped to the server ceiling, and the MIME is read from the\nfile's magic bytes — never from its name or extension.\n\n`signature` is deliberately not accepted here: it is drawn, and the canvas yields a\n`data:image/png;base64,…` value that goes straight into `data`.\n\nScope: `forms:write` + `Idempotency-Key` supported."
parameters: []
responses: { }
tags:
- 'Form submissions — fill a form through the API (ADR-0029)'
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
field:
type: string
description: "The target field's technical `name` (see `/schema`)."
example: national_card
file:
type: string
format: binary
description: 'The file itself — multipart/form-data.'
required:
- field
- file
parameters:
-
in: path
name: slug
description: "The form's slug."
example: architecto
required: true
schema:
type: string
/api/v1/forms:
get:
summary: "List forms (v2 only — schema_version='2.0')"
operationId: listFormsv2OnlySchemaVersion20
description: "Paginated list of v2-schema forms owned by the calling user, most recent\nfirst. Form Builder v1 records are excluded — use the dashboard for those.\nReturns the standard `data` + `meta` + `links` envelope, consistent with the\nother list endpoints (API-G1, audit 2026-06 — previously a silent limit(100)).\n\nScope required: `forms:read`."
parameters:
-
in: query
name: page
description: 'Page number. Default 1.'
example: 16
required: false
schema:
type: integer
description: 'Page number. Default 1.'
example: 16
-
in: query
name: per_page
description: 'Items per page. Max 100. Default 50.'
example: 16
required: false
schema:
type: integer
description: 'Items per page. Max 100. Default 50.'
example: 16
responses:
401:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/token_invalid'
status: 401
code: token_invalid
title_fa: 'توکن نامعتبر است'
title_en: 'Invalid token'
request_id: 01M0TB56HSWV85A9A6QE19BEB7
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/token_invalid'
status:
type: integer
example: 401
code:
type: string
example: token_invalid
title_fa:
type: string
example: 'توکن نامعتبر است'
title_en:
type: string
example: 'Invalid token'
request_id:
type: string
example: 01M0TB56HSWV85A9A6QE19BEB7
tags:
- 'Forms — read-only access to v2 forms owned by the authenticated user.'
'/api/v1/forms/{slug}':
get:
summary: 'Show one form (metadata only — schema available via dedicated /schema endpoint in future)'
operationId: showOneFormmetadataOnlySchemaAvailableViaDedicatedschemaEndpointInFuture
description: 'Scope required: `forms:read`.'
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data:
id: 142
slug: frm-e7465df28dd45785
title: 'قرارداد همکاری'
status: published
renderer_type: online
submission_count: 23
views_count: 187
created_at: '2026-05-10T14:32:00.000Z'
properties:
data:
type: object
properties:
id:
type: integer
example: 142
slug:
type: string
example: frm-e7465df28dd45785
title:
type: string
example: 'قرارداد همکاری'
status:
type: string
example: published
renderer_type:
type: string
example: online
submission_count:
type: integer
example: 23
views_count:
type: integer
example: 187
created_at:
type: string
example: '2026-05-10T14:32:00.000Z'
404:
description: 'Not found or not owned'
content:
application/json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/form_not_found'
status: 404
code: form_not_found
title_fa: 'فرم یافت نشد یا متعلق به شما نیست'
title_en: 'Form not found or not owned by the caller'
request_id: 01KRH8JRC4Y855P10CYC1C0AYS
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/form_not_found'
status:
type: integer
example: 404
code:
type: string
example: form_not_found
title_fa:
type: string
example: 'فرم یافت نشد یا متعلق به شما نیست'
title_en:
type: string
example: 'Form not found or not owned by the caller'
request_id:
type: string
example: 01KRH8JRC4Y855P10CYC1C0AYS
tags:
- 'Forms — read-only access to v2 forms owned by the authenticated user.'
parameters:
-
in: path
name: slug
description: "The form's public slug."
example: frm-e7465df28dd45785
required: true
schema:
type: string
'/api/v1/forms/{slug}/stats':
get:
summary: 'Form analytics (views/starts/submissions/conversion rates).'
operationId: formAnalyticsviewsstartssubmissionsconversionRates
description: "Aggregated funnel counters for one form: lifetime totals plus 7-day and 30-day windows, so\nyou can chart trends without additional queries.\n\nWARNING: Two engines count submissions here, and the response names both.\n\n- `submissions` — the lifetime counter, incremented once per accepted submission. It never\ndecreases, so a deleted submission stays counted, and it is written in exactly one place, so a\nrow created by any other path is invisible to it. **This is the v1 meaning and it does not\nchange without a version bump.** `submissions_recorded` is the same integer under a name that\nsays so.\n- `submissions_stored` — non-draft rows held right now. This is the number the owner sees in\ntheir panel, and it can differ from `submissions` in either direction.\n\n`abandons`, `completion_rate` and `abandon_rate` all derive from `submissions`, so the funnel\nadds up. `completion_rate_stored` is `null` when stored rows exceed recorded starts: the two\ncounters disagree, and a ratio across them would be drift rather than a measurement.\n\nAdded 2026-08-20 — `submissions_recorded`, `submissions_stored`, `completion_rate_stored`.\nPurely additive; no existing field changed meaning or value.\n\nScope required: `forms:read`."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data:
views: 187
starts: 41
abandons: 18
submissions: 23
submissions_recorded: 23
submissions_stored: 25
start_rate: 21.9
completion_rate: 56.1
completion_rate_stored: 61.0
abandon_rate: 43.9
last_submission_at: '2026-08-19T11:02:44+00:00'
submissions_last_7d: 4
submissions_last_30d: 21
properties:
data:
type: object
properties:
views:
type: integer
example: 187
starts:
type: integer
example: 41
abandons:
type: integer
example: 18
submissions:
type: integer
example: 23
submissions_recorded:
type: integer
example: 23
submissions_stored:
type: integer
example: 25
start_rate:
type: number
example: 21.9
completion_rate:
type: number
example: 56.1
completion_rate_stored:
type: number
example: 61.0
abandon_rate:
type: number
example: 43.9
last_submission_at:
type: string
example: '2026-08-19T11:02:44+00:00'
submissions_last_7d:
type: integer
example: 4
submissions_last_30d:
type: integer
example: 21
404:
description: 'Not found or not owned'
content:
application/json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/form_not_found'
status: 404
code: form_not_found
title_fa: 'فرم یافت نشد یا متعلق به شما نیست'
title_en: 'Form not found or not owned by the caller'
request_id: 01KRH8JRC4Y855P10CYC1C0AYS
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/form_not_found'
status:
type: integer
example: 404
code:
type: string
example: form_not_found
title_fa:
type: string
example: 'فرم یافت نشد یا متعلق به شما نیست'
title_en:
type: string
example: 'Form not found or not owned by the caller'
request_id:
type: string
example: 01KRH8JRC4Y855P10CYC1C0AYS
tags:
- 'Forms — read-only access to v2 forms owned by the authenticated user.'
parameters:
-
in: path
name: slug
description: "The form's public slug."
example: frm-e7465df28dd45785
required: true
schema:
type: string
'/api/v1/forms/{slug}/schema':
get:
summary: 'Form schema (fillable-field catalog)'
operationId: formSchemafillableFieldCatalog
description: "Everything an integrator needs to build their own fill UI and construct a valid\n`POST /forms/{slug}/submissions` payload: field names, types, labels, required flags,\noptions, pages, visibility conditions, and — per field — the SHAPE its value must take\n(`value_format`).\n\n`supported` used to report `false` for file/image \"because the web fill flow does not\nsupport them either\". Both halves were wrong: the web renderer has had real uploads\nsince 2026-06-22, and nothing ever rejected an inline base64 value on the API side. It\nnow reports true for every fillable field, and `value_format` carries the real answer:\n\n - `upload_path_or_data_uri` — file/image: either the `path` from\n `POST /forms/{slug}/uploads`, or an inline `data:;base64,…`\n - `data_uri` — signature: what the canvas produces\n - `verify_field_result` — query fields: from `POST /forms/{slug}/verify-field`\n - `scalar` — everything else\n\n**`id` is the key to keep.** It is generated once and never changes — not when the\nfield is renamed, relabelled, reordered or moved. Key your local copy of a form by\n`id`, never by `name`: a client keyed by name cannot tell a RENAME apart from \"one\nfield deleted, one added\", and those call for opposite actions. `repeat.config.ref`\nand the `fill_flow` sections both reference fields by this same id.\n\n`pages` describes what each field's `page` points at, and is never empty.\n\n`fill_flow` is present only for `image_overlay` forms (null otherwise) and says\nwhich of the two presentations the owner chose: `document` draws fields at absolute\ncoordinates on a page bitmap, while `guided` turns them into ordered question cards\n— with `doc` saying whether the document is shown alongside. Only `guided` is\nreasonable to reproduce outside our own renderer.\n\nScope required: `forms:read`."
parameters: []
responses:
200:
description: 'An online form with one text field'
content:
application/json:
schema:
type: object
example:
data:
form:
slug: frm-e7465df28dd45785
title: 'قرارداد همکاری'
version: 12
pages:
-
id: page_a1b2c3d4e5f60718
title: 'مشخصات فردی'
description: null
sort_order: 0
visibility_condition: null
fill_flow: null
fields:
-
id: fld_9a3f1c72e4b58d06
name: full_name
type: text
label: 'نام و نام خانوادگی'
required: true
is_input: true
supported: true
value_format: scalar
page: page_a1b2c3d4e5f60718
help_text: null
placeholder: null
default_value: null
options: null
config: { }
has_visibility_condition: false
visibility_condition: null
is_formula: false
formula: null
validations: []
sort_order: 0
properties:
data:
type: object
properties:
form:
type: object
properties:
slug:
type: string
example: frm-e7465df28dd45785
title:
type: string
example: 'قرارداد همکاری'
version:
type: integer
example: 12
pages:
type: array
example:
-
id: page_a1b2c3d4e5f60718
title: 'مشخصات فردی'
description: null
sort_order: 0
visibility_condition: null
items:
type: object
properties:
id:
type: string
example: page_a1b2c3d4e5f60718
title:
type: string
example: 'مشخصات فردی'
description:
type: string
example: null
nullable: true
sort_order:
type: integer
example: 0
visibility_condition:
type: string
example: null
nullable: true
fill_flow:
type: string
example: null
description: 'Image-overlay forms only (`null` otherwise): `style` is `document` or `guided`, `doc` is `live` or `hidden`, plus the ordered `sections` and any `unassigned` field ids.'
fields:
type: array
example:
-
id: fld_9a3f1c72e4b58d06
name: full_name
type: text
label: 'نام و نام خانوادگی'
required: true
is_input: true
supported: true
value_format: scalar
page: page_a1b2c3d4e5f60718
help_text: null
placeholder: null
default_value: null
options: null
config: []
has_visibility_condition: false
visibility_condition: null
is_formula: false
formula: null
validations: []
sort_order: 0
items:
type: object
properties:
id:
type: string
example: fld_9a3f1c72e4b58d06
name:
type: string
example: full_name
type:
type: string
example: text
label:
type: string
example: 'نام و نام خانوادگی'
required:
type: boolean
example: true
is_input:
type: boolean
example: true
supported:
type: boolean
example: true
value_format:
type: string
example: scalar
page:
type: string
example: page_a1b2c3d4e5f60718
help_text:
type: string
example: null
nullable: true
placeholder:
type: string
example: null
nullable: true
default_value:
type: string
example: null
nullable: true
options:
type: string
example: null
nullable: true
config:
type: object
properties: { }
has_visibility_condition:
type: boolean
example: false
visibility_condition:
type: string
example: null
nullable: true
is_formula:
type: boolean
example: false
formula:
type: string
example: null
nullable: true
validations:
type: array
example: []
sort_order:
type: integer
example: 0
tags:
- 'Forms — read-only access to v2 forms owned by the authenticated user.'
parameters:
-
in: path
name: slug
description: "The form's public slug."
example: architecto
required: true
schema:
type: string
'/api/v1/forms/{slug}/verify-field':
post:
summary: 'Verify a query field (Shahkar / legal entity)'
operationId: verifyAQueryFieldShahkarLegalEntity
description: "Runs the SAME server-side verification the web renderer's «استعلام» buttons run, and\nwrites the same server-side binding that `POST /forms/{slug}/submissions` checks — echo\nthe returned `value` back inside `data.{field}` when you submit, FROM THE SAME IP.\n\nThe form OWNER's daily Shahkar quota pays for the lookup (identical to web). Sandbox\ntokens short-circuit to `matched=true` without any provider call.\n\nScope required: `forms:write`."
parameters: []
responses: { }
tags:
- 'Forms — read-only access to v2 forms owned by the authenticated user.'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
field:
type: string
description: "The query field's name."
example: architecto
values:
type: object
description: 'Raw values of the referenced input fields.'
example:
mobile: '09121234567'
national_code: '0010350829'
properties: { }
required:
- field
- values
parameters:
-
in: path
name: slug
description: "The form's public slug."
example: architecto
required: true
schema:
type: string
/api/v1/health:
get:
summary: 'Health check'
operationId: healthCheck
description: "Lightweight liveness probe. **No authentication required.**\nAlways returns 200 if the API process is up."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
status: ok
time: '2026-05-11T11:53:29.399Z'
version: v1.0.0
properties:
status:
type: string
example: ok
time:
type: string
example: '2026-05-11T11:53:29.399Z'
version:
type: string
example: v1.0.0
tags:
- 'Meta — health, identity, quota, cost'
security: []
/api/v1/me:
get:
summary: 'Caller identity'
operationId: callerIdentity
description: "Returns information about the authenticated token, its owner user, and\nthe billing context (wallet balance, org root). Any valid token can call this —\nno specific scope required.\n\nUse the `token.mode` field to confirm whether your token is in **live** mode\n(charges your wallet, real KYC/SMS) or **test** sandbox mode (zero wallet\nimpact, mocked side effects). `wallet_balance` is always in tomans (IRR/10)."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Live token'
type: object
example:
data:
token:
id: '42'
name: production-server-2026
mode: live
scopes:
- 'contracts:read'
- 'contracts:write'
- 'webhooks:manage'
ip_whitelist:
- 203.0.113.5/32
expires_at: '2027-01-01T00:00:00.000Z'
last_used_at: '2026-05-13T18:00:42.000Z'
user:
id: 1234
name: 'آرش بنائیان چمله'
type: personal
is_org_root: false
org_root_id: null
billing_actor_user_id: 1234
wallet_balance: 849310
properties:
data:
type: object
properties:
token:
type: object
properties:
id:
type: string
example: '42'
name:
type: string
example: production-server-2026
mode:
type: string
example: live
description: 'Either `live` or `test`. Sandbox tokens never debit the wallet.'
scopes:
type: array
example:
- 'contracts:read'
- 'contracts:write'
- 'webhooks:manage'
description: 'of granted abilities. `["*"]` means full access.'
items:
type: string
ip_whitelist:
type: array
example:
- 203.0.113.5/32
description: 'If non-empty, requests from other IPs are rejected with 403 `ip_not_allowed`. Five failures within 1h auto-revoke the token.'
items:
type: string
expires_at:
type: string
example: '2027-01-01T00:00:00.000Z'
last_used_at:
type: string
example: '2026-05-13T18:00:42.000Z'
user:
type: object
properties:
id:
type: integer
example: 1234
name:
type: string
example: 'آرش بنائیان چمله'
type:
type: string
example: personal
is_org_root:
type: boolean
example: false
description: 'True when this user is the root of a Sub-Organization tree (can invite sub-users).'
org_root_id:
type: string
example: null
description: 'ID of the org root if this user is a member, else null.'
billing_actor_user_id:
type: integer
example: 1234
description: 'The wallet that pays for this request. Same as `user.id` for solo users; may be `org_root_id` for org members with `auto_charge_from_parent=true`.'
wallet_balance:
type: integer
example: 849310
description: 'Current wallet balance in tomans (Iranian Rials ÷ 10).'
-
description: 'Sandbox/test token'
type: object
example:
data:
token:
id: '13'
name: ci-tests-sandbox
mode: test
scopes:
- '*'
ip_whitelist: []
expires_at: null
last_used_at: '2026-05-13T17:52:23.000Z'
user:
id: 1234
name: 'آرش بنائیان چمله'
type: personal
is_org_root: false
org_root_id: null
billing_actor_user_id: 1234
wallet_balance: 849310
properties:
data:
type: object
properties:
token:
type: object
properties:
id:
type: string
example: '13'
name:
type: string
example: ci-tests-sandbox
mode:
type: string
example: test
description: 'Either `live` or `test`. Sandbox tokens never debit the wallet.'
scopes:
type: array
example:
- '*'
description: 'of granted abilities. `["*"]` means full access.'
items:
type: string
ip_whitelist:
type: array
example: []
description: 'If non-empty, requests from other IPs are rejected with 403 `ip_not_allowed`. Five failures within 1h auto-revoke the token.'
expires_at:
type: string
example: null
nullable: true
last_used_at:
type: string
example: '2026-05-13T17:52:23.000Z'
user:
type: object
properties:
id:
type: integer
example: 1234
name:
type: string
example: 'آرش بنائیان چمله'
type:
type: string
example: personal
is_org_root:
type: boolean
example: false
description: 'True when this user is the root of a Sub-Organization tree (can invite sub-users).'
org_root_id:
type: string
example: null
description: 'ID of the org root if this user is a member, else null.'
billing_actor_user_id:
type: integer
example: 1234
description: 'The wallet that pays for this request. Same as `user.id` for solo users; may be `org_root_id` for org members with `auto_charge_from_parent=true`.'
wallet_balance:
type: integer
example: 849310
description: 'Current wallet balance in tomans (Iranian Rials ÷ 10).'
tags:
- 'Meta — health, identity, quota, cost'
/api/v1/quota:
get:
summary: 'Current quota'
operationId: currentQuota
description: "Returns the caller's current-month signature quota usage + remaining,\npackage tier, and today's Shahkar verification credits.\n\n**Quota semantics:**\n- `free_quota` is the per-period allowance from the user's active package\n (Free=3 lifetime, Bronze=100/mo, Silver=300/mo, Gold=600/mo — verify against\n live `/admin/packages`, these are the 2026-06 tariff). Returns 0 if the package\n is expired or unset.\n- `used`/`remaining` may be FRACTIONAL: additional signers on one contract count\n as half a unit each (a 2-signer contract consumes 1.5). Whole values serialize\n as integers; a half consumes serializes as e.g. `1.5`.\n- `used` is total signature-units recorded since `package_started_at` (so\n pre-purchase usage doesn't deplete a freshly-bought plan). Free tier counts\n lifetime (no monthly reset).\n- `remaining = free_quota - used` for limited plans. **null** means\n genuinely unlimited (`signature_limit` IS NULL).\n- When `remaining` reaches 0, signatures still work — billed as overage from the\n wallet at `base × (1 − overage_discount)`; the OVERAGE discount is\n Free=0%, Bronze=5%, Silver=15%, Gold=25% (separate from, and smaller than, the\n 10/20/30% in-package discount encoded in the package price).\n\n**Shahkar credits** are separate from signature quota. Daily allowance\nby package (Free=3, Bronze=5, Silver=10, Gold=20). Once exhausted, the\nclient must purchase batch top-ups before further Shahkar verifications.\n\nScope required: `contracts:read`."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Bronze user mid-month'
type: object
example:
data:
service: electronic_signature
period_start: '2026-05-01T00:00:00.000Z'
period_end: '2026-05-31T23:59:59.999Z'
package:
slug: bronze
name: برنزی
free_quota: 30
used: 17
remaining: 13
shahkar:
free_limit: 5
used_today: 2
free_remaining: 3
extra_credits: 0
extra_remaining: 0
allowed: true
properties:
data:
type: object
properties:
service:
type: string
example: electronic_signature
period_start:
type: string
example: '2026-05-01T00:00:00.000Z'
period_end:
type: string
example: '2026-05-31T23:59:59.999Z'
package:
type: object
properties:
slug:
type: string
example: bronze
name:
type: string
example: برنزی
free_quota:
type: integer
example: 30
description: 'Monthly signature allowance from active package. Zero if package expired or none.'
used:
type: integer
example: 17
description: 'Signatures recorded this period (since package_started_at).'
remaining:
type: integer
example: 13
description: 'Signatures left this period. `null` means unlimited (Gold).'
shahkar:
type: object
properties:
free_limit:
type: integer
example: 5
used_today:
type: integer
example: 2
free_remaining:
type: integer
example: 3
extra_credits:
type: integer
example: 0
extra_remaining:
type: integer
example: 0
allowed:
type: boolean
example: true
description: 'Whether the user may attempt a Shahkar verification right now. False = quota exhausted, must purchase batch top-up.'
-
description: 'Gold (unlimited)'
type: object
example:
data:
service: electronic_signature
period_start: '2026-05-01T00:00:00.000Z'
period_end: '2026-05-31T23:59:59.999Z'
package:
slug: gold
name: طلایی
free_quota: 0
used: 412
remaining: null
shahkar:
free_limit: 20
used_today: 4
free_remaining: 16
extra_credits: 0
extra_remaining: 0
allowed: true
properties:
data:
type: object
properties:
service:
type: string
example: electronic_signature
period_start:
type: string
example: '2026-05-01T00:00:00.000Z'
period_end:
type: string
example: '2026-05-31T23:59:59.999Z'
package:
type: object
properties:
slug:
type: string
example: gold
name:
type: string
example: طلایی
free_quota:
type: integer
example: 0
description: 'Monthly signature allowance from active package. Zero if package expired or none.'
used:
type: integer
example: 412
description: 'Signatures recorded this period (since package_started_at).'
remaining:
type: string
example: null
description: 'Signatures left this period. `null` means unlimited (Gold).'
shahkar:
type: object
properties:
free_limit:
type: integer
example: 20
used_today:
type: integer
example: 4
free_remaining:
type: integer
example: 16
extra_credits:
type: integer
example: 0
extra_remaining:
type: integer
example: 0
allowed:
type: boolean
example: true
description: 'Whether the user may attempt a Shahkar verification right now. False = quota exhausted, must purchase batch top-up.'
-
description: 'No active package (pay-per-use)'
type: object
example:
data:
service: electronic_signature
period_start: '2026-05-01T00:00:00.000Z'
period_end: '2026-05-31T23:59:59.999Z'
package: null
free_quota: 0
used: 0
remaining: 0
shahkar:
free_limit: 0
used_today: 0
free_remaining: 0
extra_credits: 0
extra_remaining: 0
allowed: false
properties:
data:
type: object
properties:
service:
type: string
example: electronic_signature
period_start:
type: string
example: '2026-05-01T00:00:00.000Z'
period_end:
type: string
example: '2026-05-31T23:59:59.999Z'
package:
type: string
example: null
nullable: true
free_quota:
type: integer
example: 0
description: 'Monthly signature allowance from active package. Zero if package expired or none.'
used:
type: integer
example: 0
description: 'Signatures recorded this period (since package_started_at).'
remaining:
type: integer
example: 0
description: 'Signatures left this period. `null` means unlimited (Gold).'
shahkar:
type: object
properties:
free_limit:
type: integer
example: 0
used_today:
type: integer
example: 0
free_remaining:
type: integer
example: 0
extra_credits:
type: integer
example: 0
extra_remaining:
type: integer
example: 0
allowed:
type: boolean
example: false
description: 'Whether the user may attempt a Shahkar verification right now. False = quota exhausted, must purchase batch top-up.'
tags:
- 'Meta — health, identity, quota, cost'
/api/v1/cost-estimate:
get:
summary: 'Cost estimate'
operationId: costEstimate
description: "Returns full cost breakdown for a hypothetical contract without committing.\nUse before calling `POST /contracts` so users can see \"X toman\" upfront.\n\nScope required: `contracts:read`."
parameters:
-
in: query
name: signers
description: 'Number of signers (1-20).'
example: 3
required: true
schema:
type: integer
description: 'Number of signers (1-20).'
example: 3
-
in: query
name: discount_code
description: 'Optional discount code.'
example: WELCOME10
required: false
schema:
type: string
description: 'Optional discount code.'
example: WELCOME10
responses:
401:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/token_invalid'
status: 401
code: token_invalid
title_fa: 'توکن نامعتبر است'
title_en: 'Invalid token'
request_id: 01M0TB56CK30WTDC7J030ZXPFV
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/token_invalid'
status:
type: integer
example: 401
code:
type: string
example: token_invalid
title_fa:
type: string
example: 'توکن نامعتبر است'
title_en:
type: string
example: 'Invalid token'
request_id:
type: string
example: 01M0TB56CK30WTDC7J030ZXPFV
tags:
- 'Meta — health, identity, quota, cost'
'/api/v1/contracts/{code}/signers':
get:
summary: 'List signers'
operationId: listSigners
description: "Lists all signers on a contract. PII (mobile, national_code) is masked\nunless the caller's token has `signers:read_pii` scope.\n\nScope: `contracts:read`."
parameters: []
responses:
404:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/not_found'
status: 404
code: not_found
title_fa: 'منبع یافت نشد'
title_en: 'Resource not found'
request_id: 01M0TB56GKFB93AA71XDX16ZQ5
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/not_found'
status:
type: integer
example: 404
code:
type: string
example: not_found
title_fa:
type: string
example: 'منبع یافت نشد'
title_en:
type: string
example: 'Resource not found'
request_id:
type: string
example: 01M0TB56GKFB93AA71XDX16ZQ5
tags:
- Signers
post:
summary: 'Add signers'
operationId: addSigners
description: "Adds one or more signers to a draft contract. Runs **mandatory** Shahkar\npre-validation (mobile ↔ national_code match) per signer in production.\nOn success, contract transitions to `waiting_signature` and SMS dispatch\njobs are queued for first-order signers.\n\nScope: `signers:write`."
parameters: []
responses: { }
tags:
- Signers
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
signers:
type: array
description: 'Array of signer objects.'
example:
- architecto
items:
type: string
required:
- signers
parameters:
-
in: path
name: code
description: '16-char contract code.'
example: architecto
required: true
schema:
type: string
'/api/v1/contracts/{code}/signers/{slug}':
get:
summary: 'Get signer'
operationId: getSigner
description: "Detailed view of a single signer. PII masked unless token has `signers:read_pii`.\n\nScope: `contracts:read`."
parameters: []
responses:
404:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/not_found'
status: 404
code: not_found
title_fa: 'منبع یافت نشد'
title_en: 'Resource not found'
request_id: 01M0TB56GWSQ659J9SK71B68G7
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/not_found'
status:
type: integer
example: 404
code:
type: string
example: not_found
title_fa:
type: string
example: 'منبع یافت نشد'
title_en:
type: string
example: 'Resource not found'
request_id:
type: string
example: 01M0TB56GWSQ659J9SK71B68G7
tags:
- Signers
delete:
summary: 'Remove signer'
operationId: removeSigner
description: "Removes a signer from a contract. Only allowed when:\n- Signer status is `waiting` (not yet signed)\n- Contract status is `draft` or `waiting_signature`\n\nScope: `signers:write`."
parameters: []
responses:
204:
description: ''
content:
application/json:
schema:
type: object
example: { }
properties: { }
tags:
- Signers
parameters:
-
in: path
name: code
description: '16-char contract code.'
example: architecto
required: true
schema:
type: string
-
in: path
name: slug
description: 'Per-signer slug.'
example: DEF456GHJ789KLM
required: true
schema:
type: string
'/api/v1/contracts/{code}/signers/{slug}/resend':
post:
summary: 'Resend signing link SMS'
operationId: resendSigningLinkSMS
description: "Re-sends the signing-link SMS to a waiting signer. Rate-limited to 3 per\n5 minutes per signer (returns 429 `rate_limit_exceeded`).\n\nScope: `signers:write`."
parameters: []
responses: { }
tags:
- Signers
parameters:
-
in: path
name: code
description: '16-char contract code.'
example: architecto
required: true
schema:
type: string
-
in: path
name: slug
description: 'Per-signer slug.'
example: architecto
required: true
schema:
type: string
/api/v1/templates:
get:
summary: 'List templates'
operationId: listTemplates
description: "Lists contract templates the caller can use.\n\n`view_scope` options:\n- `self` — own templates only (default)\n- `shared_from_parent` — own + templates shared by org parent/root\n- `all_org` — entire org tree (org root only)\n\nScope: `templates:read`."
parameters:
-
in: query
name: view_scope
description: 'self | shared_from_parent | all_org. Default self.'
example: architecto
required: false
schema:
type: string
description: 'self | shared_from_parent | all_org. Default self.'
example: architecto
-
in: query
name: active
description: 'Filter by active status.'
example: false
required: false
schema:
type: boolean
description: 'Filter by active status.'
example: false
-
in: query
name: page
description: ''
example: 16
required: false
schema:
type: integer
description: ''
example: 16
-
in: query
name: per_page
description: 'Max 100. Default 20.'
example: 16
required: false
schema:
type: integer
description: 'Max 100. Default 20.'
example: 16
responses:
401:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/token_invalid'
status: 401
code: token_invalid
title_fa: 'توکن نامعتبر است'
title_en: 'Invalid token'
request_id: 01M0TB56Q4V9KMNG657H9VRAJ5
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/token_invalid'
status:
type: integer
example: 401
code:
type: string
example: token_invalid
title_fa:
type: string
example: 'توکن نامعتبر است'
title_en:
type: string
example: 'Invalid token'
request_id:
type: string
example: 01M0TB56Q4V9KMNG657H9VRAJ5
tags:
- Templates
'/api/v1/templates/{id}':
get:
summary: 'Get template'
operationId: getTemplate
description: "Full template shape with embedded positions + page geometry + sample preview link.\n\nScope: `templates:read`."
parameters: []
responses:
404:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/not_found'
status: 404
code: not_found
title_fa: 'منبع یافت نشد'
title_en: 'Resource not found'
request_id: 01M0TB56QFAPZ7H9RXZN7YJPKJ
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/not_found'
status:
type: integer
example: 404
code:
type: string
example: not_found
title_fa:
type: string
example: 'منبع یافت نشد'
title_en:
type: string
example: 'Resource not found'
request_id:
type: string
example: 01M0TB56QFAPZ7H9RXZN7YJPKJ
tags:
- Templates
parameters:
-
in: path
name: id
description: 'The ID of the template.'
example: architecto
required: true
schema:
type: string
-
in: path
name: template
description: 'Template ID.'
example: 16
required: true
schema:
type: integer
'/api/v1/templates/{template}/preview/{page}':
get:
summary: 'Template page preview'
operationId: templatePagePreview
description: "**NOT IMPLEMENTED YET** — returns `501 preview_not_available`. The signed\npage-preview URL lands in a later phase (Storage::temporaryUrl). Documented\nas a stub so merchants do not code against a fake `url:null` 200.\n\nScope: `templates:read`."
parameters: []
responses:
401:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/token_invalid'
status: 401
code: token_invalid
title_fa: 'توکن نامعتبر است'
title_en: 'Invalid token'
request_id: 01M0TB56QR43AMCAN2EM1QGE91
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/token_invalid'
status:
type: integer
example: 401
code:
type: string
example: token_invalid
title_fa:
type: string
example: 'توکن نامعتبر است'
title_en:
type: string
example: 'Invalid token'
request_id:
type: string
example: 01M0TB56QR43AMCAN2EM1QGE91
501:
description: 'Stub — not implemented'
content:
application/json:
schema:
type: object
example:
status: 501
code: preview_not_available
properties:
status:
type: integer
example: 501
code:
type: string
example: preview_not_available
tags:
- Templates
parameters:
-
in: path
name: template
description: 'Template ID.'
example: 16
required: true
schema:
type: integer
-
in: path
name: page
description: 'Page number (1-indexed).'
example: 16
required: true
schema:
type: integer
/api/v1/webhooks:
get:
summary: 'List webhook subscriptions'
operationId: listWebhookSubscriptions
description: ''
parameters:
-
in: query
name: active
description: 'Filter by active state.'
example: false
required: false
schema:
type: boolean
description: 'Filter by active state.'
example: false
-
in: query
name: page
description: ''
example: 16
required: false
schema:
type: integer
description: ''
example: 16
-
in: query
name: per_page
description: 'Max 100.'
example: 16
required: false
schema:
type: integer
description: 'Max 100.'
example: 16
responses:
401:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/token_invalid'
status: 401
code: token_invalid
title_fa: 'توکن نامعتبر است'
title_en: 'Invalid token'
request_id: 01M0TB56SHQJ6EZQ4CSVZ2Y3EV
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/token_invalid'
status:
type: integer
example: 401
code:
type: string
example: token_invalid
title_fa:
type: string
example: 'توکن نامعتبر است'
title_en:
type: string
example: 'Invalid token'
request_id:
type: string
example: 01M0TB56SHQJ6EZQ4CSVZ2Y3EV
tags:
- Webhooks
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
active:
type: boolean
description: ''
example: true
nullable: true
page:
type: integer
description: 'value باید حداقل 1 باشد.'
example: 16
nullable: true
per_page:
type: integer
description: 'value باید حداقل 1 باشد. value نباید بیشتر از 100 باشد.'
example: 22
nullable: true
post:
summary: 'Create webhook subscription'
operationId: createWebhookSubscription
description: "Registers an HTTPS endpoint to receive event deliveries. We POST each\nmatching event to your URL and sign it with HMAC-SHA256:\n\n X-Emza-Timestamp: \n X-Emza-Signature: sha256=\n\nCompare with `hash_equals`, and reject anything whose timestamp is more than 5 minutes old.\n\nNOTE: this docblock is published to customers by Scribe. Until 2026-07-31 it described a\nStripe-style `X-Emza-Signature: t=,v1=` header that this platform has never sent —\nsee App\\Services\\Webhook\\WebhookSigner, which is the only authority on the wire format, and\ntests/Feature/Sdk/SdkWebhookVerificationTest, which pins the two together.\n\n**The plain signing `secret` is returned ONCE — store it immediately.**\nSubsequent reads (`index`, `show`) omit the secret entirely. Use\n`PATCH /webhooks/{id}` with `rotate_secret=true` to issue a fresh secret\n(invalidates the previous one immediately).\n\n**URL validation (SSRF guard):** the `url` field is rejected with `422`\nif it fails any of:\n - Non-HTTPS scheme (`http://`, `ftp://`, `javascript:`, `file://`)\n - Loopback IPs (`127.0.0.0/8`, `::1`)\n - RFC 1918 private (`10/8`, `172.16/12`, `192.168/16`)\n - Link-local + cloud metadata (`169.254.0.0/16`, `100.100.100.200`)\n - IPv6 link-local (`fe80::/10`) or unique-local (`fc00::/7`)\n - Hostname suffix `.local`, `.internal`, `.private`, `.lan` or `localhost`\n - Hostname whose DNS resolves to any of the above\n\n**Allowed event names** (set in `events` array — use `[\"*\"]` for all):\n`contract.created`, `contract.processing.completed`, `contract.processing.failed`,\n`contract.signer.added`, `contract.signer.authenticated`, `contract.signer.signed`,\n`contract.signer.rejected`, `contract.completed`, `contract.canceled`,\n`contract.refunded`, `*` (wildcard).\n\nIf `*` is present alongside specific events, the specifics are dropped\n(the wildcard makes them redundant). Same event listed twice is deduped.\n\nScope: `webhooks:manage`. Idempotency-Key required (24h replay window)."
parameters: []
responses:
201:
description: ''
content:
application/json:
schema:
type: object
example:
data:
id: 01krh7z0e8a4n39gf2ary73xbj
url: 'https://example.com/webhooks/emza'
events:
- contract.completed
- contract.signer.signed
active: true
disabled_at: null
failed_deliveries_count: 0
last_delivery_at: null
last_delivery_status: null
created_at: '2026-05-13T18:00:42.000Z'
updated_at: '2026-05-13T18:00:42.000Z'
secret: 35ada3b45aa00df901b74b2b483114315a0c21f7c096f17e0bb29ab7d907fe32
_warning: 'Save this secret now — it will NOT be shown again.'
properties:
data:
type: object
properties:
id:
type: string
example: 01krh7z0e8a4n39gf2ary73xbj
url:
type: string
example: 'https://example.com/webhooks/emza'
events:
type: array
example:
- contract.completed
- contract.signer.signed
items:
type: string
active:
type: boolean
example: true
disabled_at:
type: string
example: null
nullable: true
failed_deliveries_count:
type: integer
example: 0
last_delivery_at:
type: string
example: null
nullable: true
last_delivery_status:
type: string
example: null
nullable: true
created_at:
type: string
example: '2026-05-13T18:00:42.000Z'
updated_at:
type: string
example: '2026-05-13T18:00:42.000Z'
secret:
type: string
example: 35ada3b45aa00df901b74b2b483114315a0c21f7c096f17e0bb29ab7d907fe32
_warning:
type: string
example: 'Save this secret now — it will NOT be shown again.'
422:
description: 'SSRF blocked URL'
content:
application/json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/validation'
status: 422
code: validation
title_fa: 'اعتبارسنجی ورودی شکست خورد'
title_en: 'Request validation failed'
errors:
url:
- 'آدرسهای داخلی شبکه (localhost / *.local / *.internal) مجاز نیستند.'
request_id: 01KRH8JRC4Y855P10CYC1C0AYS
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/validation'
status:
type: integer
example: 422
code:
type: string
example: validation
title_fa:
type: string
example: 'اعتبارسنجی ورودی شکست خورد'
title_en:
type: string
example: 'Request validation failed'
errors:
type: object
properties:
url:
type: array
example:
- 'آدرسهای داخلی شبکه (localhost / *.local / *.internal) مجاز نیستند.'
items:
type: string
request_id:
type: string
example: 01KRH8JRC4Y855P10CYC1C0AYS
tags:
- Webhooks
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
url:
type: string
description: 'HTTPS endpoint to receive deliveries. Max 500 chars.'
example: 'https://example.com/webhooks/emza'
events:
type: array
description: 'Event names from the allowed list. Use `["*"]` for all.'
example:
- contract.completed
- contract.signer.signed
items:
type: string
active:
type: boolean
description: 'When false, subscription is paused (no deliveries). Default true.'
example: false
required:
- url
- events
'/api/v1/webhooks/{id}':
get:
summary: 'Get webhook subscription'
operationId: getWebhookSubscription
description: ''
parameters: []
responses:
401:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/token_invalid'
status: 401
code: token_invalid
title_fa: 'توکن نامعتبر است'
title_en: 'Invalid token'
request_id: 01M0TB56SVEZ9C45TYK3QK1750
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/token_invalid'
status:
type: integer
example: 401
code:
type: string
example: token_invalid
title_fa:
type: string
example: 'توکن نامعتبر است'
title_en:
type: string
example: 'Invalid token'
request_id:
type: string
example: 01M0TB56SVEZ9C45TYK3QK1750
tags:
- Webhooks
patch:
summary: 'Update webhook subscription'
operationId: updateWebhookSubscription
description: "Partial update — any field omitted is left unchanged. New `url` values\npass through the same SSRF guard as `POST /webhooks` (see Create endpoint).\n\nWhen `rotate_secret=true`, a fresh HMAC signing secret is generated and\nreturned in the response — the previous secret is invalidated immediately,\nso any in-flight or already-sent webhook deliveries signed with the old\nsecret will fail verification on the receiver side. Plan rotations during\na quiet period or pause via `active=false` first.\n\nScope: `webhooks:manage`. Idempotency-Key required (24h replay window)."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data:
id: 01krh7z0e8a4n39gf2ary73xbj
url: 'https://example.com/webhooks/emza'
events:
- '*'
active: false
failed_deliveries_count: 2
last_delivery_at: '2026-05-13T17:30:00.000Z'
last_delivery_status: delivered
properties:
data:
type: object
properties:
id:
type: string
example: 01krh7z0e8a4n39gf2ary73xbj
url:
type: string
example: 'https://example.com/webhooks/emza'
events:
type: array
example:
- '*'
items:
type: string
active:
type: boolean
example: false
failed_deliveries_count:
type: integer
example: 2
last_delivery_at:
type: string
example: '2026-05-13T17:30:00.000Z'
last_delivery_status:
type: string
example: delivered
tags:
- Webhooks
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
url:
type: string
description: 'New endpoint URL (subject to same SSRF guard as create).'
example: 'http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html'
events:
type: array
description: 'Replaces the entire events list. Same allowed values as create.'
example:
- architecto
items:
type: string
active:
type: boolean
description: 'Toggle the subscription on/off without deleting it.'
example: false
rotate_secret:
type: boolean
description: 'When true, issues a fresh secret (returned in response under `secret` + `_warning`).'
example: false
delete:
summary: 'Delete webhook subscription'
operationId: deleteWebhookSubscription
description: "Soft-deletes the subscription. In-flight deliveries that have already\nbeen queued will complete; no new deliveries are dispatched."
parameters: []
responses: { }
tags:
- Webhooks
parameters:
-
in: path
name: id
description: 'The ID of the webhook.'
example: B8c6F7NNfcj9m6RJYC6RtkmZN6
required: true
schema:
type: string
/api/v1/webhook-deliveries:
get:
summary: 'List webhook deliveries'
operationId: listWebhookDeliveries
description: ''
parameters:
-
in: query
name: subscription_id
description: 'Filter by subscription.'
example: architecto
required: false
schema:
type: string
description: 'Filter by subscription.'
example: architecto
-
in: query
name: event_name
description: 'Filter by event name.'
example: architecto
required: false
schema:
type: string
description: 'Filter by event name.'
example: architecto
-
in: query
name: status
description: pending|delivered|failed|dead-letter
example: architecto
required: false
schema:
type: string
description: pending|delivered|failed|dead-letter
example: architecto
-
in: query
name: page
description: ''
example: 16
required: false
schema:
type: integer
description: ''
example: 16
-
in: query
name: per_page
description: 'Max 100.'
example: 16
required: false
schema:
type: integer
description: 'Max 100.'
example: 16
responses:
401:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/token_invalid'
status: 401
code: token_invalid
title_fa: 'توکن نامعتبر است'
title_en: 'Invalid token'
request_id: 01M0TB56TMQ5DM02AAXFJY67D0
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/token_invalid'
status:
type: integer
example: 401
code:
type: string
example: token_invalid
title_fa:
type: string
example: 'توکن نامعتبر است'
title_en:
type: string
example: 'Invalid token'
request_id:
type: string
example: 01M0TB56TMQ5DM02AAXFJY67D0
tags:
- Webhooks
'/api/v1/webhook-deliveries/{id}':
get:
summary: 'Get webhook delivery'
operationId: getWebhookDelivery
description: 'Shows full payload, response code, error message, attempt history.'
parameters: []
responses:
401:
description: ''
content:
application/problem+json:
schema:
type: object
example:
type: 'https://docs.elemza.com/errors/token_invalid'
status: 401
code: token_invalid
title_fa: 'توکن نامعتبر است'
title_en: 'Invalid token'
request_id: 01M0TB56V0MKCWTNVDCQ1QR7SG
properties:
type:
type: string
example: 'https://docs.elemza.com/errors/token_invalid'
status:
type: integer
example: 401
code:
type: string
example: token_invalid
title_fa:
type: string
example: 'توکن نامعتبر است'
title_en:
type: string
example: 'Invalid token'
request_id:
type: string
example: 01M0TB56V0MKCWTNVDCQ1QR7SG
tags:
- Webhooks
parameters:
-
in: path
name: id
description: 'The ID of the webhook delivery.'
example: B8c6F7NNfcj9m6RJYC6RtkmZN6
required: true
schema:
type: string
'/api/v1/webhooks/{id}/test':
post:
summary: 'Send a synthetic test event'
operationId: sendASyntheticTestEvent
description: "Fires a `webhook.test` event to verify the endpoint is reachable + signed\ncorrectly. Counts toward `webhook_deliveries` like a real event."
parameters: []
responses: { }
tags:
- Webhooks
parameters:
-
in: path
name: id
description: 'The ID of the webhook.'
example: B8c6F7NNfcj9m6RJYC6RtkmZN6
required: true
schema:
type: string
'/api/v1/webhook-deliveries/{id}/replay':
post:
summary: 'Replay webhook delivery'
operationId: replayWebhookDelivery
description: "Creates a NEW delivery row with the same payload + subscription. The\noriginal row keeps its terminal status (failed / dead-letter / delivered)."
parameters: []
responses: { }
tags:
- Webhooks
parameters:
-
in: path
name: id
description: 'The ID of the webhook delivery.'
example: B8c6F7NNfcj9m6RJYC6RtkmZN6
required: true
schema:
type: string