Introduction
Public REST API for Contract creation and Electronic Signature lifecycle. Bilingual (fa/en) errors. RFC 7807 problem+json envelope.
## Quickstart
1. **Create a Personal Access Token** in your Emza dashboard at `/dashboard/api-tokens`. The plain token is shown **once at creation** — copy and store it securely.
2. The token format is `{id}|{secret}` (Sanctum Personal Access Token), e.g. `42|abcdef1234567890…`. Send it verbatim — no `pat_live_` prefix, no extra wrapping.
3. Send any API request with `Authorization: Bearer 42|abcdef1234567890…`.
4. Your token's **mode** (Live vs. Test/Sandbox) is shown in the `data.token.mode` field of `GET /api/v1/me`. Sandbox tokens never charge your wallet — usage is no-op.
## Headers
| Header | Purpose |
|--------|---------|
| `Authorization: Bearer <token>` | Required on all authenticated routes |
| `Idempotency-Key: <key>` | Required on every POST/PATCH/DELETE (see Idempotency section) |
| `Accept: application/json` | Recommended; ensures JSON responses on all paths |
| `X-Skip-Sms: true` | (POST /contracts only) skip emza's outbound SMS; you deliver signing_url yourself |
## Idempotency
All `POST`, `PATCH`, `PUT`, `DELETE` endpoints support **Idempotency-Key** for safe retries.
- **Format:** `[A-Za-z0-9_-]+`, max 64 chars (e.g. `pay-2026-05-13-001` or any ULID).
- **TTL:** 24 hours. Within that window, replaying the same key + same body returns the **original response** with `X-Idempotent-Replay: true` (status code preserved).
- **Body conflict:** Same key with a different body returns `409 idempotency_conflict`.
- **Malformed key:** 65+ chars or characters outside `[A-Za-z0-9_-]` returns `400 malformed_idempotency_key`.
- **Missing key:** Allowed for backwards compat — request proceeds without replay protection. Recommended to always send one.
## Rate limits
Per-token, tier-aware. Every response includes `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` headers. When exceeded, the response is `429 rate_limit_exceeded` with `Retry-After` header.
| Tier | Requests / minute |
|------|------:|
| Anonymous (`/health`, signed download URL) | 60 (per IP) |
| Free / no active package | 30 (per token) |
| Bronze | 120 |
| Silver | 600 |
| Gold | 3,000 |
## Response envelope
**Success** — wrapped in `data`:
```json
{ "data": { ... } }
```
Paginated lists add `meta` + `links`:
```json
{
"data": [...],
"meta": { "current_page": 1, "per_page": 20, "total": 142 },
"links": { "next": "https://elemza.com/api/v1/contracts?page=2" }
}
```
**Error** — RFC 7807 problem+json envelope with bilingual titles:
```json
{
"type": "https://docs.elemza.com/errors/token_invalid",
"status": 401,
"code": "token_invalid",
"title_fa": "توکن نامعتبر است",
"title_en": "Invalid token",
"request_id": "01KRH69QCTCWD0RWP2NRR05VZ5"
}
```
Field semantics:
- `type` — stable URI identifying the error class. Treat as constant; UI may follow for help docs.
- `status` — HTTP status code (duplicates the response line; convenient for log pipelines).
- `code` — machine-readable error identifier. **Always present.** Use this for branching.
- `title_fa` / `title_en` — human-readable message in both languages. Safe to display directly to end-users.
- `request_id` — ULID echoed in `X-Request-ID` response header. Quote this when contacting support.
Validation errors additionally include `errors` (field-keyed array of messages):
```json
{
"status": 422, "code": "validation",
"errors": { "url": ["فقط آدرسهای HTTPS مجاز هستند."] },
...
}
```
## Error codes catalog
| code | HTTP | Meaning |
|------|:----:|---------|
| `token_missing` | 401 | `Authorization: Bearer …` header is missing or empty |
| `token_invalid` | 401 | Token doesn't match any active row (typo, deleted, malformed) |
| `token_revoked` | 401 | Token was revoked (manually or auto after 5 IP-whitelist strikes) |
| `token_expired` | 401 | Token's `expires_at` is in the past |
| `token_missing` | 401 | No `Authorization: Bearer` header was sent |
| `ip_not_allowed` | 403 | Request IP doesn't match the token's IP whitelist |
| `scope_missing` | 403 | Token lacks the ability needed for this endpoint (e.g. `signers:read_pii` for PII filters) |
| `policy_denied` | 403 | Authenticated but not authorized for this specific resource |
| `not_found` | 404 | Resource doesn't exist, was deleted, or isn't owned by the caller |
| `method_not_allowed` | 405 | HTTP method not supported on this path |
| `insufficient_balance` | 402 | Wallet balance too low for the requested operation |
| `idempotency_conflict` | 409 | Idempotency-Key was already used with a different request body |
| `idempotency_in_progress` | 409 | A concurrent request with the same Idempotency-Key is still processing; retry shortly |
| `validation` | 422 | Body or query parameters failed validation (see `errors` field) |
| `invalid_page` | 422 | Requested page number is out of range |
| `invalid_format` | 422 | A `format`/option parameter had an unsupported value |
| `not_ready` | 422 | The requested artifact (e.g. signed PDF) is not generated yet |
| `malformed_idempotency_key` | 400 | Idempotency-Key value has wrong format |
| `rate_limit_exceeded` | 429 | Tier limit reached. Retry after `Retry-After` seconds |
| `preview_not_available` | 501 | Endpoint is a documented stub not yet implemented (e.g. template page preview) |
| `internal_error` | 500 | Unexpected server fault. `request_id` is logged on our side; report when frequent |
## Webhooks
Subscribe to async events via `POST /webhooks`. We deliver an HTTP `POST` to your URL with HMAC-SHA256 signature in `X-Emza-Signature: t=<unix>,v1=<hex>`.
**Event names** (set in `events` array; use `["*"]` for all):
| Event | When |
|-------|------|
| `contract.created` | Contract record created (before file processing) |
| `contract.processing.completed` | Background file → PDF job finished |
| `contract.processing.failed` | File processing failed; reason in payload |
| `contract.signer.added` | New signer attached to contract |
| `contract.signer.authenticated` | Signer completed OTP / KYC |
| `contract.signer.signed` | Signer placed signature/stamp |
| `contract.signer.rejected` | Signer canceled their participation |
| `contract.completed` | All signers signed + signed PDF generated |
| `contract.canceled` | Owner canceled the contract |
| `contract.refunded` | Cost refunded to wallet (auto on cancel) |
**URL restrictions (SSRF guard):** webhook URLs are validated server-side before save. The following are rejected with `422`:
- Non-HTTPS schemes (`http://`, `ftp://`, `javascript:`, `file://`)
- Loopback IPs (`127.0.0.0/8`, `::1`)
- RFC 1918 private (`10/8`, `172.16/12`, `192.168/16`)
- Link-local + cloud metadata (`169.254.0.0/16`, `100.100.100.200`)
- IPv6 link-local (`fe80::/10`) and unique-local (`fc00::/7`)
- Hostnames ending in `.local`, `.internal`, `.private`, `.lan`, or `localhost`
- Hosts whose DNS resolves to any of the above
**Verifying signatures:** see `app/Services/Webhook/` examples or the Webhook section below.
<aside>Full architecture + decision log lives at <code>docs/api-master-plan/</code> in the Emza repo. Stakeholder-locked plan as of 2026-05-11.</aside>
Authenticating requests
To authenticate requests, include an Authorization header with the value "Bearer 42|abcdef1234567890abcdef1234567890abcdef".
All authenticated endpoints are marked with a requires authentication badge in the documentation below.
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 patlive prefix.
Contracts
Public signed-URL download.
requires authentication
Validates Laravel's signed middleware. No Bearer token required —
the URL signature is the auth proof. The signature is generated by
downloadPdf() after the bearer was verified, so this is functionally
equivalent to a 24h time-bounded capability URL.
Records an ApiRequestLog-style entry via the underlying ContractDownload
audit table (same trail as the user-panel signed-link download flow).
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/contracts/architecto/dl" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/contracts/architecto/dl"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (404):
Show headers
x-request-id: 01M0TB569B06B153MHVXBVGBW2
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/not_found",
"status": 404,
"code": "not_found",
"title_fa": "منبع یافت نشد",
"title_en": "Resource not found",
"request_id": "01M0TB569B06B153MHVXBVGBW2"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List contracts
requires authentication
Returns paginated contracts visible to caller. Org root sees own + descendants' non-private contracts; non-root sees own + contracts where they're a signer.
Scope: contracts:read.
PII filter scope requirement: filtering by signer_mobile or
signer_national_code requires the additional signers:read_pii ability
on your token. Without it, those query parameters return
403 scope_missing. This prevents a low-privilege contracts:read
token from using the index as a confirmation oracle to check whether a
given mobile/national-code signs any contract on the owner's tree.
The * wildcard scope also passes the check.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/contracts?status=waiting_signature&signature_method=digital&created_after=2026-01-01T00%3A00%3A00Z&created_before=architecto&signer_mobile=09121234567&signer_national_code=architecto&template_id=16&is_private=&page=16&per_page=16&sort=architecto" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"status\": \"waiting_signature\",
\"signature_method\": \"digital\",
\"created_after\": \"2026-08-24T20:25:15\",
\"created_before\": \"2026-08-24T20:25:15\",
\"signer_mobile\": \"09564255931\",
\"signer_national_code\": \"ikhwaykcmy\",
\"template_id\": 16,
\"is_private\": true,
\"page\": 22,
\"per_page\": 7,
\"sort\": \"created_at\"
}"
const url = new URL(
"https://elemza.com/api/v1/contracts"
);
const params = {
"status": "waiting_signature",
"signature_method": "digital",
"created_after": "2026-01-01T00:00:00Z",
"created_before": "architecto",
"signer_mobile": "09121234567",
"signer_national_code": "architecto",
"template_id": "16",
"is_private": "0",
"page": "16",
"per_page": "16",
"sort": "architecto",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"status": "waiting_signature",
"signature_method": "digital",
"created_after": "2026-08-24T20:25:15",
"created_before": "2026-08-24T20:25:15",
"signer_mobile": "09564255931",
"signer_national_code": "ikhwaykcmy",
"template_id": 16,
"is_private": true,
"page": 22,
"per_page": 7,
"sort": "created_at"
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());Example response (401):
Show headers
x-request-id: 01M0TB56ECVSX0CKVWKG3R4TNJ
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/token_invalid",
"status": 401,
"code": "token_invalid",
"title_fa": "توکن نامعتبر است",
"title_en": "Invalid token",
"request_id": "01M0TB56ECVSX0CKVWKG3R4TNJ"
}
Example response (403, PII filter without scope):
{
"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"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get contract
requires authentication
Returns full contract shape with embedded signers, per-page geometry, and template positions snapshot.
Scope: contracts:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/contracts/ABC1234567XYZ890" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/contracts/ABC1234567XYZ890"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (401):
Show headers
x-request-id: 01M0TB56EQYEJPZ984WW270GM5
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/token_invalid",
"status": 401,
"code": "token_invalid",
"title_fa": "توکن نامعتبر است",
"title_en": "Invalid token",
"request_id": "01M0TB56EQYEJPZ984WW270GM5"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Download signed PDF
requires authentication
Returns the final signed PDF in one of three formats (Phase 3):
format=redirect(default) —302 Foundto a signed URL (24h TTL). The signed URL points at the unauthenticated/api/v1/contracts/{code}/dlroute, which validates the signature and streams the file. Use this when embedding in<a href>for partner-facing UIs.format=url— JSON envelope withurl,expires_at,filename,size_bytes,checksum_sha256. Use this when the API client wants to hand the URL to another system, embed in email, or pre-flight before downloading.format=stream— streams the PDF inline (consumes API egress). Use when the caller is a backend service that wants the bytes in one round-trip.
Scope: contracts:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/contracts/architecto/pdf?format=url&ttl=16" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/contracts/architecto/pdf"
);
const params = {
"format": "url",
"ttl": "16",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (404):
Show headers
x-request-id: 01M0TB56F7FRJNS78ATMT67JVA
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/not_found",
"status": 404,
"code": "not_found",
"title_fa": "منبع یافت نشد",
"title_en": "Resource not found",
"request_id": "01M0TB56F7FRJNS78ATMT67JVA"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Poll processing status
requires authentication
Lightweight (<200 bytes) status of the rasterization + signer-creation job.
Reads contract:progress:{id} cache key directly — no DB hit.
Step values: files, signers, sms, done, error.
Returns ready=true when step=done. If progress key is gone (TTL expired)
and contract has signers + pages, falls back to ready=true (job finished
earlier and key was reaped).
Scope: contracts:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/contracts/architecto/processing" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/contracts/architecto/processing"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (404):
Show headers
x-request-id: 01M0TB56FMBZKHZN0ZY16GMJAC
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/not_found",
"status": 404,
"code": "not_found",
"title_fa": "منبع یافت نشد",
"title_en": "Resource not found",
"request_id": "01M0TB56FMBZKHZN0ZY16GMJAC"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create contract
requires authentication
Creates a new contract. Two modes:
One-shot — multipart/form-data with file field + metadata. File is
persisted to staging and ProcessContractFilesJob is dispatched. Optional
signers[] are added by the same job after page rasterization completes.
Response is 202 Accepted; poll _links.processing until ready=true.
Multi-step — JSON without file. Creates a draft contract row only.
Caller then issues POST /contracts/{code}/files to attach the document.
Signature placement — template_id + template_enforcement
Passing template_id snapshots that template's signature slots onto the new
contract. template_enforcement decides what those slots MEAN to the signer:
locked— each slot is pinned: the signer cannot drag the signature off it, and cannot sign on a page the template never assigned them. This is the default whenevertemplate_idis present.suggested— the signature merely starts on the slot; the signer may move it anywhere, on any page.
strict / loose are accepted as legacy aliases of locked / suggested
(the spelling earlier revisions of this page advertised).
If the template pins slots on more than one page, send all_pages_signature=true
as well — otherwise one signature on any allowed page completes the contract.
Verify what was applied on the create response (template_id,
template_enforcement) or in full via GET /contracts/{code} → template_positions.
Scope: contracts:write.
Example request:
curl --request POST \
"https://elemza.com/api/v1/contracts" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "title=قرارداد بیمه شخص ثالث"\
--form "signature_method=architecto"\
--form "all_pages_signature="\
--form "is_private="\
--form "template_id=26"\
--form "template_enforcement=locked"\
--form "redirect_url=https://partner.example/contracts/42/signed"\
--form "signers[]=architecto"\
--form "file=@C:\Users\pc\AppData\Local\Temp\php95E9.tmp" const url = new URL(
"https://elemza.com/api/v1/contracts"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('title', 'قرارداد بیمه شخص ثالث');
body.append('signature_method', 'architecto');
body.append('all_pages_signature', '');
body.append('is_private', '');
body.append('template_id', '26');
body.append('template_enforcement', 'locked');
body.append('redirect_url', 'https://partner.example/contracts/42/signed');
body.append('signers[]', 'architecto');
body.append('file', document.querySelector('input[name="file"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());Example response (422, template_id not reachable by the caller):
{
"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"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Upload contract file(s) (two-step flow)
requires authentication
Attaches a file to an existing draft contract that has no pages yet.
Same async pipeline as POST /contracts one-shot: stages bytes, dispatches
ProcessContractFilesJob, returns 202 with the processing link.
Scope: contracts:write.
Example request:
curl --request POST \
"https://elemza.com/api/v1/contracts/architecto/files" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "file=@C:\Users\pc\AppData\Local\Temp\php95F9.tmp" const url = new URL(
"https://elemza.com/api/v1/contracts/architecto/files"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('file', document.querySelector('input[name="file"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Cancel contract
requires authentication
Cancels a contract in draft or waiting_signature state. Refunds the
billed amount to the original wallet (100% if no signer signed, 50% if any
Shahkar was consumed). Deletes contract files from storage. Waiting signers
receive an SMS notifying them the contract was canceled.
Scope: contracts:write.
Example request:
curl --request DELETE \
"https://elemza.com/api/v1/contracts/architecto" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/contracts/architecto"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());Example response (200):
{
"data": {
"code": "ABC1234567XYZ890",
"status": "canceled",
"canceled_at": "2026-05-11T10:00:00.000Z",
"refund": {
"amount": 60000,
"currency": "IRT"
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Digital certificates (PKI)
Issuing a certificate is not something an API can do on somebody's behalf: the law wants an OTP that person typed and a live face check they attended. So these endpoints do the part an API legitimately can — start the enrolment, hand back a link for the person, and let the integrator follow it to the end.
Certificate status for one of your people
requires authentication
Answers "can this person sign digitally yet?" for somebody you have enrolled.
Privacy — this is not a lookup service for arbitrary national codes. It only answers for people you yourself started an enrolment for; anyone else returns 404, so the endpoint cannot be used to discover whether a given Iranian holds a certificate.
Scope required: certificates:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/certificates?national_code=0010350829" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/certificates"
);
const params = {
"national_code": "0010350829",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (200, Active certificate):
{
"data": {
"national_code": "0010350829",
"status": "active",
"valid_from": "2026-07-30T09:12:00.000Z",
"valid_until": "2027-07-30T09:12:00.000Z",
"mode": "live"
}
}
Example response (200, Enrolment still in progress):
{
"data": {
"national_code": "0010350829",
"status": "pending_ekyc",
"valid_from": null,
"valid_until": null,
"mode": "live"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
One enrolment request
requires authentication
The state of a single enrolment you started, by the request_id you received.
Scope required: certificates:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/certificates/01jqk8x2m4n6p8r0s2t4v6w8y0" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/certificates/01jqk8x2m4n6p8r0s2t4v6w8y0"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (401):
Show headers
x-request-id: 01M0TB56P8HCD9VKZECE1SNVS5
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/token_invalid",
"status": 401,
"code": "token_invalid",
"title_fa": "توکن نامعتبر است",
"title_en": "Invalid token",
"request_id": "01M0TB56P8HCD9VKZECE1SNVS5"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Start a certificate enrolment
requires authentication
Creates a short-lived, single-use link for one of your users to obtain a digital certificate.
Send them to onboarding_url; they complete the SMS code, the identity form and the video
check on our pages, and you learn the outcome from certificate.issued / certificate.failed
(or by polling GET /api/v1/certificates/{request_id}).
Who pays — pay_by: "issuer" charges YOUR wallet for the one-time issuance fee right now
and refunds it automatically if no certificate is ever issued. pay_by: "subject" (default)
lets the person pay on the page.
Idempotency — while an enrolment for the same national code is still open you get the SAME link back, not a second one. Pendar treats an open enrolment as a single order.
Scope required: certificates:write.
Example request:
curl --request POST \
"https://elemza.com/api/v1/certificates/onboarding" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"national_code\": \"0010350829\",
\"mobile\": \"09121234567\",
\"first_name\": \"علی\",
\"last_name\": \"رضایی\",
\"pay_by\": \"issuer\",
\"redirect_url\": \"https:\\/\\/partner.example\\/done\",
\"ttl_minutes\": 120
}"
const url = new URL(
"https://elemza.com/api/v1/certificates/onboarding"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"national_code": "0010350829",
"mobile": "09121234567",
"first_name": "علی",
"last_name": "رضایی",
"pay_by": "issuer",
"redirect_url": "https:\/\/partner.example\/done",
"ttl_minutes": 120
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());Example response (201, Link created):
{
"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"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Form submissions — fill a form through the API (ADR-0029)
Submit-once semantics: no draft/resume, no edit-after-submit (FB SCOPE-CONTRACT rows 6/7). Ownership: every endpoint requires the form to BELONG to the token owner (v2 schema only) — the integrator drives fills of their OWN forms; end users never hold tokens.
Public signed-URL download twin (no Bearer — the URL signature is the auth proof, minted by downloadPdf() after the bearer was verified). Mirrors contracts /dl.
requires authentication
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/form-submissions/architecto/dl" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/form-submissions/architecto/dl"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (404):
Show headers
x-request-id: 01M0TB56B2TW4QKAHSPA045ACY
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/not_found",
"status": 404,
"code": "not_found",
"title_fa": "منبع یافت نشد",
"title_en": "Resource not found",
"request_id": "01M0TB56B2TW4QKAHSPA045ACY"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Public signed-URL twin for a single field file (no Bearer — the signature is the auth proof, minted by downloadFieldFile() only after the bearer was verified).
requires authentication
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/form-submissions/architecto/files/architecto/dl" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/form-submissions/architecto/files/architecto/dl"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (404):
Show headers
x-request-id: 01M0TB56BDPVRXEKC0F5PRAYFA
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/not_found",
"status": 404,
"code": "not_found",
"title_fa": "منبع یافت نشد",
"title_en": "Resource not found",
"request_id": "01M0TB56BDPVRXEKC0F5PRAYFA"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List submissions
requires authentication
Paginated, newest first. Scope: forms:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/forms/architecto/submissions?status=architecto&submitted_after=architecto&submitted_before=architecto&has_contract=&per_page=16" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"status\": \"submitted\",
\"submitted_after\": \"2026-08-24T20:25:15\",
\"submitted_before\": \"2026-08-24T20:25:15\",
\"has_contract\": true,
\"page\": 16,
\"per_page\": 22
}"
const url = new URL(
"https://elemza.com/api/v1/forms/architecto/submissions"
);
const params = {
"status": "architecto",
"submitted_after": "architecto",
"submitted_before": "architecto",
"has_contract": "0",
"per_page": "16",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"status": "submitted",
"submitted_after": "2026-08-24T20:25:15",
"submitted_before": "2026-08-24T20:25:15",
"has_contract": true,
"page": 16,
"per_page": 22
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());Example response (401):
Show headers
x-request-id: 01M0TB56K1MBR6KBMRYXPDZBE3
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/token_invalid",
"status": 401,
"code": "token_invalid",
"title_fa": "توکن نامعتبر است",
"title_en": "Invalid token",
"request_id": "01M0TB56K1MBR6KBMRYXPDZBE3"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get one submission
requires authentication
Scope: forms:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/forms/architecto/submissions/architecto" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/forms/architecto/submissions/architecto"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (404):
Show headers
x-request-id: 01M0TB56KHGP7765916R7C299G
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/not_found",
"status": 404,
"code": "not_found",
"title_fa": "منبع یافت نشد",
"title_en": "Resource not found",
"request_id": "01M0TB56KHGP7765916R7C299G"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Download submission PDF
requires authentication
Image-overlay forms render a final PDF per submission (queued at submit). Same signed-URL family as the contracts PDF endpoint:
format=redirect(default) — 302 to a signed URL (anon-safe, forwardable)format=url— JSON envelope with url/expiry/checksumformat=stream— bytes in this response
Scope: forms:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/forms/architecto/submissions/architecto/pdf?format=architecto&ttl=16" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/forms/architecto/submissions/architecto/pdf"
);
const params = {
"format": "architecto",
"ttl": "16",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (404):
Show headers
x-request-id: 01M0TB56M1X2AGEBJVV6DPVMAM
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/not_found",
"status": 404,
"code": "not_found",
"title_fa": "منبع یافت نشد",
"title_en": "Resource not found",
"request_id": "01M0TB56M1X2AGEBJVV6DPVMAM"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Download one uploaded field file
requires authentication
Serves whatever the submission actually holds for {field}: a stored forms/… path,
OR an inline data:…;base64,… URI. Both shapes are real and both are served — the
data-URI is what a drawn signature is, and what API callers necessarily wrote before
an uploads endpoint existed, so refusing it would orphan real historical data.
The field's type is resolved from the submission's OWN schema snapshot, not the live form: editing a form must never change what an already-recorded submission is understood to contain.
Same three formats as the submission PDF endpoint.
Scope: forms:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/forms/architecto/submissions/architecto/files/architecto?format=architecto&ttl=16" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/forms/architecto/submissions/architecto/files/architecto"
);
const params = {
"format": "architecto",
"ttl": "16",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (404):
Show headers
x-request-id: 01M0TB56ME91KZHB7W3SNX629B
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/not_found",
"status": 404,
"code": "not_found",
"title_fa": "منبع یافت نشد",
"title_en": "Resource not found",
"request_id": "01M0TB56ME91KZHB7W3SNX629B"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Submit a form (fill via API)
requires authentication
Runs the EXACT same pipeline as the public web renderer: schema validation,
visibility filtering, formula computation, trust scoring, the signature bridge
(requires_signature forms auto-create a contract — response embeds it with per-signer
signing_urls), image-overlay PDF rendering (queued), and owner notifications.
Scope: forms:write + Idempotency-Key supported.
Fields hidden by visibility conditions are stripped server-side. query_shahkar /
query_legal_entity field values must come from POST /forms/{slug}/verify-field
(same request IP) or their matched=true claim is stripped as a forgery.
file / image fields take EITHER of two shapes, both real:
- the
pathreturned byPOST /forms/{slug}/uploads— preferred, stores the bytes on disk exactly as the web fill flow does, and is readable back via the/files/{field}endpoint; - an inline
data:<mime>;base64,…URI, which is also what a drawnsignatureis. Both are magic-byte validated and size-capped server-side. (This docblock claimed uploads were "not supported in v1 (mirrors the web fill flow)" until 2026-08-14 — the second half had been untrue since 2026-06-22, when the web renderer got real uploads, and the first half was never enforced by any code: base64 always passed.)
Example request:
curl --request POST \
"https://elemza.com/api/v1/forms/architecto/submissions" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"data\": {
\"first_name\": \"علی\"
}
}"
const url = new URL(
"https://elemza.com/api/v1/forms/architecto/submissions"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"data": {
"first_name": "علی"
}
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Upload a file for a `file` / `image` field
requires authentication
Multipart, ONE file per call. The returned path is what you then put in
data.{field} on POST /forms/{slug}/submissions. The bytes land in the form's own
folder byte-identically to what the web fill flow produces, so the submissions table,
the answer sheet and the image-overlay PDF all render it the same way — an API fill
and a browser fill become indistinguishable downstream, which is the whole point.
Size and type come from the FIELD's own config (max_size_kb / allowed_mimes as the
designer set them), clamped to the server ceiling, and the MIME is read from the
file's magic bytes — never from its name or extension.
signature is deliberately not accepted here: it is drawn, and the canvas yields a
data:image/png;base64,… value that goes straight into data.
Scope: forms:write + Idempotency-Key supported.
Example request:
curl --request POST \
"https://elemza.com/api/v1/forms/architecto/uploads" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "field=national_card"\
--form "file=@C:\Users\pc\AppData\Local\Temp\php9589.tmp" const url = new URL(
"https://elemza.com/api/v1/forms/architecto/uploads"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('field', 'national_card');
body.append('file', document.querySelector('input[name="file"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Forms — read-only access to v2 forms owned by the authenticated user.
Phase 5 MVP: list + show + stats. Submissions endpoint will land in Phase 5b with proper pagination + transformer.
List forms (v2 only — schema_version='2.0')
requires authentication
Paginated list of v2-schema forms owned by the calling user, most recent
first. Form Builder v1 records are excluded — use the dashboard for those.
Returns the standard data + meta + links envelope, consistent with the
other list endpoints (API-G1, audit 2026-06 — previously a silent limit(100)).
Scope required: forms:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/forms?page=16&per_page=16" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/forms"
);
const params = {
"page": "16",
"per_page": "16",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (401):
Show headers
x-request-id: 01M0TB56HSWV85A9A6QE19BEB7
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/token_invalid",
"status": 401,
"code": "token_invalid",
"title_fa": "توکن نامعتبر است",
"title_en": "Invalid token",
"request_id": "01M0TB56HSWV85A9A6QE19BEB7"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
status
One of draft, published, archived.
renderer_type
Form rendering mode: online (web form) or image_overlay (PDF background + overlay).
Show one form (metadata only — schema available via dedicated /schema endpoint in future)
requires authentication
Scope required: forms:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/forms/frm-e7465df28dd45785" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/forms/frm-e7465df28dd45785"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (200):
{
"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"
}
}
Example response (404, Not found or not owned):
{
"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"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Form analytics (views/starts/submissions/conversion rates).
requires authentication
Aggregated funnel counters for one form: lifetime totals plus 7-day and 30-day windows, so you can chart trends without additional queries.
WARNING: Two engines count submissions here, and the response names both.
submissions— the lifetime counter, incremented once per accepted submission. It never decreases, so a deleted submission stays counted, and it is written in exactly one place, so a row created by any other path is invisible to it. This is the v1 meaning and it does not change without a version bump.submissions_recordedis the same integer under a name that says so.submissions_stored— non-draft rows held right now. This is the number the owner sees in their panel, and it can differ fromsubmissionsin either direction.
abandons, completion_rate and abandon_rate all derive from submissions, so the funnel
adds up. completion_rate_stored is null when stored rows exceed recorded starts: the two
counters disagree, and a ratio across them would be drift rather than a measurement.
Added 2026-08-20 — submissions_recorded, submissions_stored, completion_rate_stored.
Purely additive; no existing field changed meaning or value.
Scope required: forms:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/forms/frm-e7465df28dd45785/stats" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/forms/frm-e7465df28dd45785/stats"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (200):
{
"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,
"abandon_rate": 43.9,
"last_submission_at": "2026-08-19T11:02:44+00:00",
"submissions_last_7d": 4,
"submissions_last_30d": 21
}
}
Example response (404, Not found or not owned):
{
"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"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Form schema (fillable-field catalog)
requires authentication
Everything an integrator needs to build their own fill UI and construct a valid
POST /forms/{slug}/submissions payload: field names, types, labels, required flags,
options, pages, visibility conditions, and — per field — the SHAPE its value must take
(value_format).
supported used to report false for file/image "because the web fill flow does not
support them either". Both halves were wrong: the web renderer has had real uploads
since 2026-06-22, and nothing ever rejected an inline base64 value on the API side. It
now reports true for every fillable field, and value_format carries the real answer:
upload_path_or_data_uri— file/image: either thepathfromPOST /forms/{slug}/uploads, or an inlinedata:<mime>;base64,…data_uri— signature: what the canvas producesverify_field_result— query fields: fromPOST /forms/{slug}/verify-fieldscalar— everything else
id is the key to keep. It is generated once and never changes — not when the
field is renamed, relabelled, reordered or moved. Key your local copy of a form by
id, never by name: a client keyed by name cannot tell a RENAME apart from "one
field deleted, one added", and those call for opposite actions. repeat.config.ref
and the fill_flow sections both reference fields by this same id.
pages describes what each field's page points at, and is never empty.
fill_flow is present only for image_overlay forms (null otherwise) and says
which of the two presentations the owner chose: document draws fields at absolute
coordinates on a page bitmap, while guided turns them into ordered question cards
— with doc saying whether the document is shown alongside. Only guided is
reasonable to reproduce outside our own renderer.
Scope required: forms:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/forms/architecto/schema" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/forms/architecto/schema"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (200, An online form with one text field):
{
"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
}
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
fields
object
id
Stable field identifier (fld_ + 16 hex). Survives renames — key your local copy by this, never by name.
formula
The expression a computed field evaluates; null for every other type.
config
Per-type constraints the designer set (e.g. max_size_kb, allowed_mimes, min_year_offset). Only keys an integrator needs; omitted when unset.
pages[]
The form's pages, in display order. Never empty.
fill_flow
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.
Verify a query field (Shahkar / legal entity)
requires authentication
Runs the SAME server-side verification the web renderer's «استعلام» buttons run, and
writes the same server-side binding that POST /forms/{slug}/submissions checks — echo
the returned value back inside data.{field} when you submit, FROM THE SAME IP.
The form OWNER's daily Shahkar quota pays for the lookup (identical to web). Sandbox
tokens short-circuit to matched=true without any provider call.
Scope required: forms:write.
Example request:
curl --request POST \
"https://elemza.com/api/v1/forms/architecto/verify-field" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"field\": \"architecto\",
\"values\": {
\"mobile\": \"09121234567\",
\"national_code\": \"0010350829\"
}
}"
const url = new URL(
"https://elemza.com/api/v1/forms/architecto/verify-field"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"field": "architecto",
"values": {
"mobile": "09121234567",
"national_code": "0010350829"
}
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Meta — health, identity, quota, cost
Health check
Lightweight liveness probe. No authentication required. Always returns 200 if the API process is up.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/health" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/health"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (200):
{
"status": "ok",
"time": "2026-05-11T11:53:29.399Z",
"version": "v1.0.0"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Caller identity
requires authentication
Returns information about the authenticated token, its owner user, and the billing context (wallet balance, org root). Any valid token can call this — no specific scope required.
Use the token.mode field to confirm whether your token is in live mode
(charges your wallet, real KYC/SMS) or test sandbox mode (zero wallet
impact, mocked side effects). wallet_balance is always in tomans (IRR/10).
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/me" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/me"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (200, Live token):
{
"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
}
}
Example response (200, Sandbox/test token):
{
"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
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
token
object
mode
Either live or test. Sandbox tokens never debit the wallet.
scopes
string[]
of granted abilities. ["*"] means full access.
ip_whitelist
If non-empty, requests from other IPs are rejected with 403 ip_not_allowed. Five failures within 1h auto-revoke the token.
user
object
is_org_root
True when this user is the root of a Sub-Organization tree (can invite sub-users).
org_root_id
ID of the org root if this user is a member, else null.
billing_actor_user_id
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
Current wallet balance in tomans (Iranian Rials ÷ 10).
Current quota
requires authentication
Returns the caller's current-month signature quota usage + remaining, package tier, and today's Shahkar verification credits.
Quota semantics:
free_quotais the per-period allowance from the user's active package (Free=3 lifetime, Bronze=100/mo, Silver=300/mo, Gold=600/mo — verify against live/admin/packages, these are the 2026-06 tariff). Returns 0 if the package is expired or unset.used/remainingmay be FRACTIONAL: additional signers on one contract count as half a unit each (a 2-signer contract consumes 1.5). Whole values serialize as integers; a half consumes serializes as e.g.1.5.usedis total signature-units recorded sincepackage_started_at(so pre-purchase usage doesn't deplete a freshly-bought plan). Free tier counts lifetime (no monthly reset).remaining = free_quota - usedfor limited plans. null means genuinely unlimited (signature_limitIS NULL).- When
remainingreaches 0, signatures still work — billed as overage from the wallet atbase × (1 − overage_discount); the OVERAGE discount is Free=0%, Bronze=5%, Silver=15%, Gold=25% (separate from, and smaller than, the 10/20/30% in-package discount encoded in the package price).
Shahkar credits are separate from signature quota. Daily allowance by package (Free=3, Bronze=5, Silver=10, Gold=20). Once exhausted, the client must purchase batch top-ups before further Shahkar verifications.
Scope required: contracts:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/quota" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/quota"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (200, Bronze user mid-month):
{
"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
}
}
}
Example response (200, Gold (unlimited)):
{
"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
}
}
}
Example response (200, No active package (pay-per-use)):
{
"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
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Response
Response Fields
data
object
free_quota
Monthly signature allowance from active package. Zero if package expired or none.
used
Signatures recorded this period (since package_started_at).
remaining
Signatures left this period. null means unlimited (Gold).
shahkar
object
allowed
Whether the user may attempt a Shahkar verification right now. False = quota exhausted, must purchase batch top-up.
Cost estimate
requires authentication
Returns full cost breakdown for a hypothetical contract without committing.
Use before calling POST /contracts so users can see "X toman" upfront.
Scope required: contracts:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/cost-estimate?signers=3&discount_code=WELCOME10" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/cost-estimate"
);
const params = {
"signers": "3",
"discount_code": "WELCOME10",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (401):
Show headers
x-request-id: 01M0TB56CK30WTDC7J030ZXPFV
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/token_invalid",
"status": 401,
"code": "token_invalid",
"title_fa": "توکن نامعتبر است",
"title_en": "Invalid token",
"request_id": "01M0TB56CK30WTDC7J030ZXPFV"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Signers
Signer endpoints for a contract.
Phase 1 = read-only (index + show). Resend + delete = Phase 2 (mutations).
Spec: docs/api-master-plan/02-endpoint-surface.md (Signer endpoints).
List signers
requires authentication
Lists all signers on a contract. PII (mobile, national_code) is masked
unless the caller's token has signers:read_pii scope.
Scope: contracts:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/contracts/architecto/signers" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/contracts/architecto/signers"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (404):
Show headers
x-request-id: 01M0TB56GKFB93AA71XDX16ZQ5
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/not_found",
"status": 404,
"code": "not_found",
"title_fa": "منبع یافت نشد",
"title_en": "Resource not found",
"request_id": "01M0TB56GKFB93AA71XDX16ZQ5"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get signer
requires authentication
Detailed view of a single signer. PII masked unless token has signers:read_pii.
Scope: contracts:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/contracts/architecto/signers/DEF456GHJ789KLM" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/contracts/architecto/signers/DEF456GHJ789KLM"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (404):
Show headers
x-request-id: 01M0TB56GWSQ659J9SK71B68G7
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/not_found",
"status": 404,
"code": "not_found",
"title_fa": "منبع یافت نشد",
"title_en": "Resource not found",
"request_id": "01M0TB56GWSQ659J9SK71B68G7"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Add signers
requires authentication
Adds one or more signers to a draft contract. Runs mandatory Shahkar
pre-validation (mobile ↔ national_code match) per signer in production.
On success, contract transitions to waiting_signature and SMS dispatch
jobs are queued for first-order signers.
Scope: signers:write.
Example request:
curl --request POST \
"https://elemza.com/api/v1/contracts/architecto/signers" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"signers\": [
\"architecto\"
]
}"
const url = new URL(
"https://elemza.com/api/v1/contracts/architecto/signers"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"signers": [
"architecto"
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Resend signing link SMS
requires authentication
Re-sends the signing-link SMS to a waiting signer. Rate-limited to 3 per
5 minutes per signer (returns 429 rate_limit_exceeded).
Scope: signers:write.
Example request:
curl --request POST \
"https://elemza.com/api/v1/contracts/architecto/signers/architecto/resend" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/contracts/architecto/signers/architecto/resend"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Remove signer
requires authentication
Removes a signer from a contract. Only allowed when:
- Signer status is
waiting(not yet signed) - Contract status is
draftorwaiting_signature
Scope: signers:write.
Example request:
curl --request DELETE \
"https://elemza.com/api/v1/contracts/architecto/signers/architecto" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/contracts/architecto/signers/architecto"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());Example response (204):
Empty response
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Templates
Template endpoints — read-only in v1.0. CRUD lands in v1.x.
Spec: docs/api-master-plan/02-endpoint-surface.md (Template endpoints).
List templates
requires authentication
Lists contract templates the caller can use.
view_scope options:
self— own templates only (default)shared_from_parent— own + templates shared by org parent/rootall_org— entire org tree (org root only)
Scope: templates:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/templates?view_scope=architecto&active=&page=16&per_page=16" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/templates"
);
const params = {
"view_scope": "architecto",
"active": "0",
"page": "16",
"per_page": "16",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (401):
Show headers
x-request-id: 01M0TB56Q4V9KMNG657H9VRAJ5
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/token_invalid",
"status": 401,
"code": "token_invalid",
"title_fa": "توکن نامعتبر است",
"title_en": "Invalid token",
"request_id": "01M0TB56Q4V9KMNG657H9VRAJ5"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get template
requires authentication
Full template shape with embedded positions + page geometry + sample preview link.
Scope: templates:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/templates/architecto" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/templates/architecto"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (404):
Show headers
x-request-id: 01M0TB56QFAPZ7H9RXZN7YJPKJ
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/not_found",
"status": 404,
"code": "not_found",
"title_fa": "منبع یافت نشد",
"title_en": "Resource not found",
"request_id": "01M0TB56QFAPZ7H9RXZN7YJPKJ"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Template page preview
requires authentication
NOT IMPLEMENTED YET — returns 501 preview_not_available. The signed
page-preview URL lands in a later phase (Storage::temporaryUrl). Documented
as a stub so merchants do not code against a fake url:null 200.
Scope: templates:read.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/templates/16/preview/16" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/templates/16/preview/16"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (401):
Show headers
x-request-id: 01M0TB56QR43AMCAN2EM1QGE91
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/token_invalid",
"status": 401,
"code": "token_invalid",
"title_fa": "توکن نامعتبر است",
"title_en": "Invalid token",
"request_id": "01M0TB56QR43AMCAN2EM1QGE91"
}
Example response (501, Stub — not implemented):
{
"status": 501,
"code": "preview_not_available"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Webhooks
Outbound webhook subscriptions. Customers create one or more endpoints, each filtered by an array of event names. Deliveries are signed with HMAC-SHA256 via the secret returned at create-time (shown only once).
Scope: webhooks:manage.
List webhook subscriptions
requires authentication
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/webhooks?active=&page=16&per_page=16" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"active\": true,
\"page\": 16,
\"per_page\": 22
}"
const url = new URL(
"https://elemza.com/api/v1/webhooks"
);
const params = {
"active": "0",
"page": "16",
"per_page": "16",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"active": true,
"page": 16,
"per_page": 22
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());Example response (401):
Show headers
x-request-id: 01M0TB56SHQJ6EZQ4CSVZ2Y3EV
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/token_invalid",
"status": 401,
"code": "token_invalid",
"title_fa": "توکن نامعتبر است",
"title_en": "Invalid token",
"request_id": "01M0TB56SHQJ6EZQ4CSVZ2Y3EV"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get webhook subscription
requires authentication
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/webhooks/B8c6F7NNfcj9m6RJYC6RtkmZN6" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/webhooks/B8c6F7NNfcj9m6RJYC6RtkmZN6"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (401):
Show headers
x-request-id: 01M0TB56SVEZ9C45TYK3QK1750
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/token_invalid",
"status": 401,
"code": "token_invalid",
"title_fa": "توکن نامعتبر است",
"title_en": "Invalid token",
"request_id": "01M0TB56SVEZ9C45TYK3QK1750"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List webhook deliveries
requires authentication
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/webhook-deliveries?subscription_id=architecto&event_name=architecto&status=architecto&page=16&per_page=16" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/webhook-deliveries"
);
const params = {
"subscription_id": "architecto",
"event_name": "architecto",
"status": "architecto",
"page": "16",
"per_page": "16",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (401):
Show headers
x-request-id: 01M0TB56TMQ5DM02AAXFJY67D0
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/token_invalid",
"status": 401,
"code": "token_invalid",
"title_fa": "توکن نامعتبر است",
"title_en": "Invalid token",
"request_id": "01M0TB56TMQ5DM02AAXFJY67D0"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get webhook delivery
requires authentication
Shows full payload, response code, error message, attempt history.
Example request:
curl --request GET \
--get "https://elemza.com/api/v1/webhook-deliveries/B8c6F7NNfcj9m6RJYC6RtkmZN6" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/webhook-deliveries/B8c6F7NNfcj9m6RJYC6RtkmZN6"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());Example response (401):
Show headers
x-request-id: 01M0TB56V0MKCWTNVDCQ1QR7SG
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/problem+json
access-control-allow-origin: *
access-control-expose-headers: X-Request-ID, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, X-Emza-API-Version, X-Idempotent-Replay
{
"type": "https://docs.elemza.com/errors/token_invalid",
"status": 401,
"code": "token_invalid",
"title_fa": "توکن نامعتبر است",
"title_en": "Invalid token",
"request_id": "01M0TB56V0MKCWTNVDCQ1QR7SG"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create webhook subscription
requires authentication
Registers an HTTPS endpoint to receive event deliveries. We POST each matching event to your URL and sign it with HMAC-SHA256:
X-Emza-Timestamp: <unix seconds>
X-Emza-Signature: sha256=<hex hash_hmac('sha256', "{timestamp}.{raw_body}", secret)>
Compare with hash_equals, and reject anything whose timestamp is more than 5 minutes old.
NOTE: this docblock is published to customers by Scribe. Until 2026-07-31 it described a
Stripe-style X-Emza-Signature: t=<unix>,v1=<hex> header that this platform has never sent —
see App\Services\Webhook\WebhookSigner, which is the only authority on the wire format, and
tests/Feature/Sdk/SdkWebhookVerificationTest, which pins the two together.
The plain signing secret is returned ONCE — store it immediately.
Subsequent reads (index, show) omit the secret entirely. Use
PATCH /webhooks/{id} with rotate_secret=true to issue a fresh secret
(invalidates the previous one immediately).
URL validation (SSRF guard): the url field is rejected with 422
if it fails any of:
- Non-HTTPS scheme (
http://,ftp://,javascript:,file://) - Loopback IPs (
127.0.0.0/8,::1) - RFC 1918 private (
10/8,172.16/12,192.168/16) - Link-local + cloud metadata (
169.254.0.0/16,100.100.100.200) - IPv6 link-local (
fe80::/10) or unique-local (fc00::/7) - Hostname suffix
.local,.internal,.private,.lanorlocalhost - Hostname whose DNS resolves to any of the above
Allowed event names (set in events array — use ["*"] for all):
contract.created, contract.processing.completed, contract.processing.failed,
contract.signer.added, contract.signer.authenticated, contract.signer.signed,
contract.signer.rejected, contract.completed, contract.canceled,
contract.refunded, * (wildcard).
If * is present alongside specific events, the specifics are dropped
(the wildcard makes them redundant). Same event listed twice is deduped.
Scope: webhooks:manage. Idempotency-Key required (24h replay window).
Example request:
curl --request POST \
"https://elemza.com/api/v1/webhooks" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"url\": \"https:\\/\\/example.com\\/webhooks\\/emza\",
\"events\": [
\"contract.completed\",
\"contract.signer.signed\"
],
\"active\": false
}"
const url = new URL(
"https://elemza.com/api/v1/webhooks"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"url": "https:\/\/example.com\/webhooks\/emza",
"events": [
"contract.completed",
"contract.signer.signed"
],
"active": false
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());Example response (201):
{
"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."
}
}
Example response (422, SSRF blocked URL):
{
"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"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update webhook subscription
requires authentication
Partial update — any field omitted is left unchanged. New url values
pass through the same SSRF guard as POST /webhooks (see Create endpoint).
When rotate_secret=true, a fresh HMAC signing secret is generated and
returned in the response — the previous secret is invalidated immediately,
so any in-flight or already-sent webhook deliveries signed with the old
secret will fail verification on the receiver side. Plan rotations during
a quiet period or pause via active=false first.
Scope: webhooks:manage. Idempotency-Key required (24h replay window).
Example request:
curl --request PATCH \
"https://elemza.com/api/v1/webhooks/01krh7z0e8a4n39gf2ary73xbj" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"url\": \"http:\\/\\/www.bailey.biz\\/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html\",
\"events\": [
\"architecto\"
],
\"active\": false,
\"rotate_secret\": false
}"
const url = new URL(
"https://elemza.com/api/v1/webhooks/01krh7z0e8a4n39gf2ary73xbj"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"url": "http:\/\/www.bailey.biz\/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html",
"events": [
"architecto"
],
"active": false,
"rotate_secret": false
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());Example response (200):
{
"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"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Delete webhook subscription
requires authentication
Soft-deletes the subscription. In-flight deliveries that have already been queued will complete; no new deliveries are dispatched.
Example request:
curl --request DELETE \
"https://elemza.com/api/v1/webhooks/B8c6F7NNfcj9m6RJYC6RtkmZN6" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/webhooks/B8c6F7NNfcj9m6RJYC6RtkmZN6"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Send a synthetic test event
requires authentication
Fires a webhook.test event to verify the endpoint is reachable + signed
correctly. Counts toward webhook_deliveries like a real event.
Example request:
curl --request POST \
"https://elemza.com/api/v1/webhooks/B8c6F7NNfcj9m6RJYC6RtkmZN6/test" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/webhooks/B8c6F7NNfcj9m6RJYC6RtkmZN6/test"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Replay webhook delivery
requires authentication
Creates a NEW delivery row with the same payload + subscription. The original row keeps its terminal status (failed / dead-letter / delivered).
Example request:
curl --request POST \
"https://elemza.com/api/v1/webhook-deliveries/B8c6F7NNfcj9m6RJYC6RtkmZN6/replay" \
--header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://elemza.com/api/v1/webhook-deliveries/B8c6F7NNfcj9m6RJYC6RtkmZN6/replay"
);
const headers = {
"Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.