MENU navbar-image

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"
}
 

Request      

GET api/v1/contracts/{code}/dl

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

code   string     

16-char public code. Example: architecto

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"
}
 

Request      

GET api/v1/contracts

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

status   string  optional    

Filter by status: draft|waiting_signature|completed|voided|canceled. Example: waiting_signature

signature_method   string  optional    

Filter by signing method: electronic|digital. Example: digital

created_after   string  optional    

ISO 8601 datetime. Example: 2026-01-01T00:00:00Z

created_before   string  optional    

ISO 8601 datetime. Example: architecto

signer_mobile   string  optional    

Iran mobile regex 09XXXXXXXXX. Requires signers:read_pii scope. Example: 09121234567

signer_national_code   string  optional    

10-digit Iran national code. Requires signers:read_pii scope. Example: architecto

template_id   integer  optional    

Filter by template. Example: 16

is_private   boolean  optional    

Filter by privacy flag. Example: false

page   integer  optional    

Default 1. Example: 16

per_page   integer  optional    

Max 100. Default 20. Example: 16

sort   string  optional    

created_at | -created_at | completed_at | -completed_at. Default -created_at. Example: architecto

Body Parameters

status   string  optional    

Example: waiting_signature

Must be one of:
  • draft
  • waiting_signature
  • completed
  • voided
  • canceled
signature_method   string  optional    

Example: digital

Must be one of:
  • electronic
  • digital
created_after   string  optional    

value یک تاریخ معتبر نیست. Example: 2026-08-24T20:25:15

created_before   string  optional    

value یک تاریخ معتبر نیست. Example: 2026-08-24T20:25:15

signer_mobile   string  optional    

Must match the regex /^09[0-9]{9}$/. Example: 09564255931

signer_national_code   string  optional    

value باید 10 کاراکتر باشد. Example: ikhwaykcmy

template_id   integer  optional    

Example: 16

is_private   boolean  optional    

Example: true

page   integer  optional    

value باید حداقل 1 باشد. Example: 22

per_page   integer  optional    

value باید حداقل 1 باشد. value نباید بیشتر از 100 باشد. Example: 7

sort   string  optional    

Example: created_at

Must be one of:
  • created_at
  • -created_at
  • completed_at
  • -completed_at

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"
}
 

Request      

GET api/v1/contracts/{code}

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

code   string     

16-char public code (A-Z, 0-9). Example: ABC1234567XYZ890

Download signed PDF

requires authentication

Returns the final signed PDF in one of three formats (Phase 3):

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"
}
 

Request      

GET api/v1/contracts/{code}/pdf

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

code   string     

16-char public code. Example: architecto

Query Parameters

format   string  optional    

redirect|url|stream. Default redirect. Example: url

ttl   integer  optional    

Signed URL lifetime in seconds (60..86400). Default 86400. Ignored when format=stream. Example: 16

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"
}
 

Request      

GET api/v1/contracts/{code}/processing

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

code   string     

16-char contract code. Example: architecto

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:

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"
}
 

Request      

POST api/v1/contracts

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

Body Parameters

title   string     

Contract title (1..120 chars). Example: قرارداد بیمه شخص ثالث

signature_method   string  optional    

electronic|digital. Default electronic. digital = PADES signature via the Pendar CA cloud-HSM. Requires the PKI feature to be enabled for your account (pre-flight via GET /medata.capabilities.pki), otherwise 403 pki_not_available. The method is contract-wide (all signers sign the same way) and IMMUTABLE after creation — to change it, cancel and recreate (ADR-0009). Example: architecto

all_pages_signature   boolean  optional    

Default false. When true, every page must carry the signature element. Example: false

is_private   boolean  optional    

Default false. When true, hides contract from org-ancestors. Example: false

template_id   integer  optional    

Optional — must be owned by the caller or shared from the org root; anything else is rejected with 422 template_not_accessible (never silently dropped). List valid ids via GET /api/v1/templates. Example: 26

template_enforcement   string  optional    

locked|suggested (legacy aliases: strict|loose). Defaults to locked when template_id is set. Example: locked

redirect_url   string  optional    

HTTPS URL to send the signer back to after they finish signing. They see our confirmation first, then return automatically. Without it they stay on our confirmation page. Example: https://partner.example/contracts/42/signed

file   file  optional    

PDF or PNG/JPG, max 4MB (multipart/form-data only). Example: C:\Users\pc\AppData\Local\Temp\php95E9.tmp

source   object  optional    

Optional document source instead of file. Currently only {type: "form_submission", code: "ABCD1234EFGH5678"} — uses the rendered PDF of one of YOUR form submissions as this contract's document (the فرم → PDF → امضا chain, works with both signature methods). 409 not_ready if the submission PDF is still rendering; 422 not_applicable for online-renderer forms (no PDF artifact).

signers   string[]  optional    

Optional inline signers (one-shot). Same shape as POST /contracts/{code}/signers.

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());

Request      

POST api/v1/contracts/{code}/files

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters

code   string     

16-char contract code. Example: architecto

Body Parameters

file   file     

PDF or PNG/JPG, max 4MB (multipart/form-data). Example: C:\Users\pc\AppData\Local\Temp\php95F9.tmp

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"
        }
    }
}
 

Request      

DELETE api/v1/contracts/{code}

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

code   string     

16-char public code. Example: architecto

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"
    }
}
 

Request      

GET api/v1/certificates

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

national_code   string     

The person's national code. Example: 0010350829

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"
}
 

Request      

GET api/v1/certificates/{id}

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The request_id from the onboarding call. Example: 01jqk8x2m4n6p8r0s2t4v6w8y0

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 payspay_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"
    }
}
 

Request      

POST api/v1/certificates/onboarding

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

national_code   string     

The person's 10-digit national code. Example: 0010350829

mobile   string     

Their mobile, 09xxxxxxxxx. Example: 09121234567

first_name   string  optional    

Optional, prefills the form. Example: علی

last_name   string  optional    

Optional, prefills the form. Example: رضایی

pay_by   string  optional    

issuer or subject (default). Example: issuer

redirect_url   string  optional    

HTTPS URL to send the person back to when they finish. Example: https://partner.example/done

ttl_minutes   integer  optional    

Link lifetime, 5–1440 (default 60). Example: 120

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"
}
 

Request      

GET api/v1/form-submissions/{code}/dl

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

code   string     

16-char submission code. Example: architecto

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"
}
 

Request      

GET api/v1/form-submissions/{code}/files/{field}/dl

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

code   string     

16-char submission code. Example: architecto

field   string     

The field's technical name. Example: architecto

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"
}
 

Request      

GET api/v1/forms/{slug}/submissions

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The form's slug. Example: architecto

Query Parameters

status   string  optional    

draft|pending_payment|submitted|processing|completed|signed. Example: architecto

submitted_after   string  optional    

ISO 8601. Example: architecto

submitted_before   string  optional    

ISO 8601. Example: architecto

has_contract   boolean  optional    

Only submissions that bridged to a contract. Example: false

per_page   integer  optional    

Max 100, default 20. Example: 16

Body Parameters

status   string  optional    

Example: submitted

Must be one of:
  • draft
  • pending_payment
  • submitted
  • processing
  • completed
  • signed
submitted_after   string  optional    

value یک تاریخ معتبر نیست. Example: 2026-08-24T20:25:15

submitted_before   string  optional    

value یک تاریخ معتبر نیست. Example: 2026-08-24T20:25:15

has_contract   boolean  optional    

Example: true

page   integer  optional    

value باید حداقل 1 باشد. Example: 16

per_page   integer  optional    

value باید حداقل 1 باشد. value نباید بیشتر از 100 باشد. Example: 22

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"
}
 

Request      

GET api/v1/forms/{slug}/submissions/{code}

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The form's slug. Example: architecto

code   string     

16-char submission code. Example: architecto

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:

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"
}
 

Request      

GET api/v1/forms/{slug}/submissions/{code}/pdf

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The form's slug. Example: architecto

code   string     

16-char submission code. Example: architecto

Query Parameters

format   string  optional    

redirect|url|stream. Default redirect. Example: architecto

ttl   integer  optional    

Signed URL lifetime seconds (60..86400). Default 86400. Example: 16

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"
}
 

Request      

GET api/v1/forms/{slug}/submissions/{code}/files/{field}

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The form's slug. Example: architecto

code   string     

16-char submission code. Example: architecto

field   string     

The field's technical name. Example: architecto

Query Parameters

format   string  optional    

redirect|url|stream. Default redirect. Example: architecto

ttl   integer  optional    

Signed URL lifetime seconds (60..86400). Default 86400. Example: 16

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:

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());

Request      

POST api/v1/forms/{slug}/submissions

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The form's slug. Example: architecto

Body Parameters

data   object     

Map of field name → value.

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());

Request      

POST api/v1/forms/{slug}/uploads

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: multipart/form-data

Accept        

Example: application/json

URL Parameters

slug   string     

The form's slug. Example: architecto

Body Parameters

field   string     

The target field's technical name (see /schema). Example: national_card

file   file     

The file itself — multipart/form-data. Example: C:\Users\pc\AppData\Local\Temp\php9589.tmp

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"
}
 

Request      

GET api/v1/forms

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

page   integer  optional    

Page number. Default 1. Example: 16

per_page   integer  optional    

Items per page. Max 100. Default 50. Example: 16

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"
}
 

Request      

GET api/v1/forms/{slug}

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The form's public slug. Example: frm-e7465df28dd45785

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.

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"
}
 

Request      

GET api/v1/forms/{slug}/stats

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The form's public slug. Example: frm-e7465df28dd45785

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:

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
            }
        ]
    }
}
 

Request      

GET api/v1/forms/{slug}/schema

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The form's public slug. Example: architecto

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());

Request      

POST api/v1/forms/{slug}/verify-field

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

slug   string     

The form's public slug. Example: architecto

Body Parameters

field   string     

The query field's name. Example: architecto

values   object     

Raw values of the referenced input fields.

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"
}
 

Request      

GET api/v1/health

Headers

Content-Type        

Example: application/json

Accept        

Example: application/json

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
    }
}
 

Request      

GET api/v1/me

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

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:

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
        }
    }
}
 

Request      

GET api/v1/quota

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

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"
}
 

Request      

GET api/v1/cost-estimate

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

signers   integer     

Number of signers (1-20). Example: 3

discount_code   string  optional    

Optional discount code. Example: WELCOME10

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"
}
 

Request      

GET api/v1/contracts/{code}/signers

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

code   string     

16-char contract code. Example: architecto

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"
}
 

Request      

GET api/v1/contracts/{code}/signers/{slug}

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

code   string     

16-char contract code. Example: architecto

slug   string     

Per-signer slug. Example: DEF456GHJ789KLM

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());

Request      

POST api/v1/contracts/{code}/signers

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

code   string     

16-char contract code. Example: architecto

Body Parameters

signers   string[]     

Array of signer objects.

first_name   string     

Example: architecto

last_name   string     

Example: architecto

mobile   string     

Iran mobile regex 09XXXXXXXXX. Example: 09121234567

national_code   string     

10-digit valid Iran national code. Example: architecto

email   string  optional    

Optional. Example: gbailey@example.net

order   integer  optional    

Default 0 (parallel signing). Example: 16

signer_type   string  optional    

natural|legal. Default natural. Example: architecto

legal_national_code   string  optional    

Required when signer_type=legal (11-digit company tax ID). Example: architecto

legal_company_name   string  optional    

Required when signer_type=legal. Example: architecto

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());

Request      

POST api/v1/contracts/{code}/signers/{slug}/resend

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

code   string     

16-char contract code. Example: architecto

slug   string     

Per-signer slug. Example: architecto

Remove signer

requires authentication

Removes a signer from a contract. Only allowed when:

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
 

Request      

DELETE api/v1/contracts/{code}/signers/{slug}

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

code   string     

16-char contract code. Example: architecto

slug   string     

Per-signer slug. Example: architecto

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:

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"
}
 

Request      

GET api/v1/templates

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

view_scope   string  optional    

self | shared_from_parent | all_org. Default self. Example: architecto

active   boolean  optional    

Filter by active status. Example: false

page   integer  optional    

Example: 16

per_page   integer  optional    

Max 100. Default 20. Example: 16

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"
}
 

Request      

GET api/v1/templates/{id}

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the template. Example: architecto

template   integer     

Template ID. Example: 16

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"
}
 

Request      

GET api/v1/templates/{template}/preview/{page}

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

template   integer     

Template ID. Example: 16

page   integer     

Page number (1-indexed). Example: 16

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"
}
 

Request      

GET api/v1/webhooks

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

active   boolean  optional    

Filter by active state. Example: false

page   integer  optional    

Example: 16

per_page   integer  optional    

Max 100. Example: 16

Body Parameters

active   boolean  optional    

Example: true

page   integer  optional    

value باید حداقل 1 باشد. Example: 16

per_page   integer  optional    

value باید حداقل 1 باشد. value نباید بیشتر از 100 باشد. Example: 22

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"
}
 

Request      

GET api/v1/webhooks/{id}

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the webhook. Example: B8c6F7NNfcj9m6RJYC6RtkmZN6

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"
}
 

Request      

GET api/v1/webhook-deliveries

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

subscription_id   string  optional    

Filter by subscription. Example: architecto

event_name   string  optional    

Filter by event name. Example: architecto

status   string  optional    

pending|delivered|failed|dead-letter Example: architecto

page   integer  optional    

Example: 16

per_page   integer  optional    

Max 100. Example: 16

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"
}
 

Request      

GET api/v1/webhook-deliveries/{id}

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the webhook delivery. Example: B8c6F7NNfcj9m6RJYC6RtkmZN6

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:

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"
}
 

Request      

POST api/v1/webhooks

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

url   string     

HTTPS endpoint to receive deliveries. Max 500 chars. Example: https://example.com/webhooks/emza

events   string[]     

Event names from the allowed list. Use ["*"] for all.

active   boolean  optional    

When false, subscription is paused (no deliveries). Default true. Example: false

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"
    }
}
 

Request      

PATCH api/v1/webhooks/{id}

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

Webhook subscription ULID. Example: 01krh7z0e8a4n39gf2ary73xbj

Body Parameters

url   string  optional    

New endpoint URL (subject to same SSRF guard as create). Example: http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html

events   string[]  optional    

Replaces the entire events list. Same allowed values as create.

active   boolean  optional    

Toggle the subscription on/off without deleting it. Example: false

rotate_secret   boolean  optional    

When true, issues a fresh secret (returned in response under secret + _warning). Example: false

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());

Request      

DELETE api/v1/webhooks/{id}

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the webhook. Example: B8c6F7NNfcj9m6RJYC6RtkmZN6

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());

Request      

POST api/v1/webhooks/{id}/test

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the webhook. Example: B8c6F7NNfcj9m6RJYC6RtkmZN6

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());

Request      

POST api/v1/webhook-deliveries/{id}/replay

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The ID of the webhook delivery. Example: B8c6F7NNfcj9m6RJYC6RtkmZN6