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 |
| `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) |
| `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 |
| `idempotency_conflict` | 409 | Idempotency-Key was already used with a different request body |
| `validation` | 422 | Body or query parameters failed validation (see `errors` field) |
| `insufficient_balance` | 422 | Wallet balance too low for the requested operation |
| `malformed_idempotency_key` | 400 | Idempotency-Key value has wrong format |
| `rate_limit_exceeded` | 429 | Tier limit reached. Retry after `Retry-After` seconds |
| `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 "http://localhost:8000/api/v1/contracts/consequatur/dl" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/api/v1/contracts/consequatur/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: 01KRHE7DDE9DMAW383Z6SS09BY
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/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": "01KRHE7DDE9DMAW383Z6SS09BY"
}
 

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: consequatur

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 "http://localhost:8000/api/v1/contracts?status=waiting_signature&created_after=2026-01-01T00%3A00%3A00Z&created_before=consequatur&signer_mobile=09121234567&signer_national_code=consequatur&template_id=17&is_private=&page=17&per_page=17&sort=consequatur" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"status\": \"waiting_signature\",
    \"created_after\": \"2026-05-13T23:20:09\",
    \"created_before\": \"2026-05-13T23:20:09\",
    \"signer_mobile\": \"09156277171\",
    \"signer_national_code\": \"sufvyvddqa\",
    \"template_id\": 17,
    \"is_private\": true,
    \"page\": 45,
    \"per_page\": 16,
    \"sort\": \"created_at\"
}"
const url = new URL(
    "http://localhost:8000/api/v1/contracts"
);

const params = {
    "status": "waiting_signature",
    "created_after": "2026-01-01T00:00:00Z",
    "created_before": "consequatur",
    "signer_mobile": "09121234567",
    "signer_national_code": "consequatur",
    "template_id": "17",
    "is_private": "0",
    "page": "17",
    "per_page": "17",
    "sort": "consequatur",
};
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",
    "created_after": "2026-05-13T23:20:09",
    "created_before": "2026-05-13T23:20:09",
    "signer_mobile": "09156277171",
    "signer_national_code": "sufvyvddqa",
    "template_id": 17,
    "is_private": true,
    "page": 45,
    "per_page": 16,
    "sort": "created_at"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (401):

Show headers
x-request-id: 01KRHE7DG7JVYWGBT0X6YJ1FNF
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/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": "01KRHE7DG7JVYWGBT0X6YJ1FNF"
}
 

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

created_after   string  optional    

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

created_before   string  optional    

ISO 8601 datetime. Example: consequatur

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: consequatur

template_id   integer  optional    

Filter by template. Example: 17

is_private   boolean  optional    

Filter by privacy flag. Example: false

page   integer  optional    

Default 1. Example: 17

per_page   integer  optional    

Max 100. Default 20. Example: 17

sort   string  optional    

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

Body Parameters

status   string  optional    

Example: waiting_signature

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

value یک تاریخ معتبر نیست. Example: 2026-05-13T23:20:09

created_before   string  optional    

value یک تاریخ معتبر نیست. Example: 2026-05-13T23:20:09

signer_mobile   string  optional    

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

signer_national_code   string  optional    

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

template_id   integer  optional    

Example: 17

is_private   boolean  optional    

Example: true

page   integer  optional    

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

per_page   integer  optional    

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

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 "http://localhost:8000/api/v1/contracts/ABC1234567XYZ890" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/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: 01KRHE7DGV0VQ6SYZH0G2SCRKR
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/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": "01KRHE7DGV0VQ6SYZH0G2SCRKR"
}
 

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 "http://localhost:8000/api/v1/contracts/consequatur/pdf?format=url&ttl=17" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/api/v1/contracts/consequatur/pdf"
);

const params = {
    "format": "url",
    "ttl": "17",
};
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: 01KRHE7DH84BEG7K28XNHF2CJW
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/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": "01KRHE7DH84BEG7K28XNHF2CJW"
}
 

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: consequatur

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: 17

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 "http://localhost:8000/api/v1/contracts/consequatur/processing" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/api/v1/contracts/consequatur/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: 01KRHE7DHN9ADP8DKGHD6GCYTV
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/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": "01KRHE7DHN9ADP8DKGHD6GCYTV"
}
 

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: consequatur

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.

Scope: contracts:write.

Example request:
curl --request POST \
    "http://localhost:8000/api/v1/contracts" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: multipart/form-data" \
    --header "Accept: application/json" \
    --form "title=قرارداد بیمه شخص ثالث"\
    --form "all_pages_signature="\
    --form "is_private="\
    --form "template_id=17"\
    --form "template_enforcement=consequatur"\
    --form "signers[]=consequatur"
const url = new URL(
    "http://localhost:8000/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('all_pages_signature', '');
body.append('is_private', '');
body.append('template_id', '17');
body.append('template_enforcement', 'consequatur');
body.append('signers[]', 'consequatur');

fetch(url, {
    method: "POST",
    headers,
    body,
}).then(response => response.json());

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: قرارداد بیمه شخص ثالث

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 caller or shared from org root. Example: 17

template_enforcement   string  optional    

strict|loose. Optional — only meaningful when template_id is set. Example: consequatur

file   file  optional    

PDF or PNG/JPG, max 4MB (multipart/form-data only).

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 \
    "http://localhost:8000/api/v1/contracts/consequatur/files" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: multipart/form-data" \
    --header "Accept: application/json" \
    --form "file=@C:\Users\pc\AppData\Local\Temp\php3138.tmp" 
const url = new URL(
    "http://localhost:8000/api/v1/contracts/consequatur/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: consequatur

Body Parameters

file   file     

PDF or PNG/JPG, max 4MB (multipart/form-data). Example: C:\Users\pc\AppData\Local\Temp\php3138.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 \
    "http://localhost:8000/api/v1/contracts/consequatur" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/api/v1/contracts/consequatur"
);

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: consequatur

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

Returns up to 100 most recent v2-schema forms owned by the calling user. Form Builder v1 records are excluded — use the dashboard for those.

Scope required: forms:read.

Example request:
curl --request GET \
    --get "http://localhost:8000/api/v1/forms" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/api/v1/forms"
);

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

Request      

GET api/v1/forms

Headers

Authorization        

Example: Bearer 42|abcdef1234567890abcdef1234567890abcdef

Content-Type        

Example: application/json

Accept        

Example: application/json

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 "http://localhost:8000/api/v1/forms/frm-e7465df28dd45785" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/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 counters returned by FormAnalyticsService::stats(). Includes lifetime totals plus 7-day and 30-day windows so you can chart trends without additional queries.

Scope required: forms:read.

Example request:
curl --request GET \
    --get "http://localhost:8000/api/v1/forms/frm-e7465df28dd45785/stats" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/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": {
        "totals": {
            "views": 187,
            "starts": 41,
            "submissions": 23,
            "conversion_rate": 56.1
        },
        "last_7_days": {
            "views": 32,
            "starts": 9,
            "submissions": 4
        },
        "last_30_days": {
            "views": 154,
            "starts": 38,
            "submissions": 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

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 "http://localhost:8000/api/v1/health" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/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 "http://localhost:8000/api/v1/me" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/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 "http://localhost:8000/api/v1/quota" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/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 "http://localhost:8000/api/v1/cost-estimate?signers=3&discount_code=WELCOME10" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"signers\": 17,
    \"discount_code\": \"mqeopfuudtdsufvyvddqa\"
}"
const url = new URL(
    "http://localhost:8000/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",
};

let body = {
    "signers": 17,
    "discount_code": "mqeopfuudtdsufvyvddqa"
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (401):

Show headers
x-request-id: 01KRHE7DF20PVTV9REMR4NSX0X
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/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": "01KRHE7DF20PVTV9REMR4NSX0X"
}
 

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

Body Parameters

signers   integer     

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

discount_code   string  optional    

value نباید بیشتر از 32 کاراکتر باشد. Example: mqeopfuudtdsufvyvddqa

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 "http://localhost:8000/api/v1/contracts/consequatur/signers" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/api/v1/contracts/consequatur/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: 01KRHE7DJA84RJNMAE59Y3SHG6
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/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": "01KRHE7DJA84RJNMAE59Y3SHG6"
}
 

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: consequatur

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 "http://localhost:8000/api/v1/contracts/consequatur/signers/DEF456GHJ789KLM" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/api/v1/contracts/consequatur/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: 01KRHE7DJRYT32HYXVANCYK7H9
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/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": "01KRHE7DJRYT32HYXVANCYK7H9"
}
 

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: consequatur

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 \
    "http://localhost:8000/api/v1/contracts/consequatur/signers" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"signers\": [
        \"consequatur\"
    ]
}"
const url = new URL(
    "http://localhost:8000/api/v1/contracts/consequatur/signers"
);

const headers = {
    "Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "signers": [
        "consequatur"
    ]
};

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: consequatur

Body Parameters

signers   string[]     

Array of signer objects.

first_name   string     

Example: consequatur

last_name   string     

Example: consequatur

mobile   string     

Iran mobile regex 09XXXXXXXXX. Example: 09121234567

national_code   string     

10-digit valid Iran national code. Example: consequatur

email   string  optional    

Optional. Example: qkunze@example.com

order   integer  optional    

Default 0 (parallel signing). Example: 17

signer_type   string  optional    

natural|legal. Default natural. Example: consequatur

legal_national_code   string  optional    

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

legal_company_name   string  optional    

Required when signer_type=legal. Example: consequatur

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 \
    "http://localhost:8000/api/v1/contracts/consequatur/signers/consequatur/resend" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/api/v1/contracts/consequatur/signers/consequatur/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: consequatur

slug   string     

Per-signer slug. Example: consequatur

Remove signer

requires authentication

Removes a signer from a contract. Only allowed when:

Scope: signers:write.

Example request:
curl --request DELETE \
    "http://localhost:8000/api/v1/contracts/consequatur/signers/consequatur" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/api/v1/contracts/consequatur/signers/consequatur"
);

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: consequatur

slug   string     

Per-signer slug. Example: consequatur

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 "http://localhost:8000/api/v1/templates?view_scope=consequatur&active=&page=17&per_page=17" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"view_scope\": \"all_org\",
    \"active\": true,
    \"page\": 73,
    \"per_page\": 13
}"
const url = new URL(
    "http://localhost:8000/api/v1/templates"
);

const params = {
    "view_scope": "consequatur",
    "active": "0",
    "page": "17",
    "per_page": "17",
};
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 = {
    "view_scope": "all_org",
    "active": true,
    "page": 73,
    "per_page": 13
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (401):

Show headers
x-request-id: 01KRHE7DMJME92CQKMASDKJ61D
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/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": "01KRHE7DMJME92CQKMASDKJ61D"
}
 

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: consequatur

active   boolean  optional    

Filter by active status. Example: false

page   integer  optional    

Example: 17

per_page   integer  optional    

Max 100. Default 20. Example: 17

Body Parameters

view_scope   string  optional    

Example: all_org

Must be one of:
  • self
  • shared_from_parent
  • all_org
active   boolean  optional    

Example: true

page   integer  optional    

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

per_page   integer  optional    

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

Get template

requires authentication

Full template shape with embedded positions + page geometry + sample preview link.

Scope: templates:read.

Example request:
curl --request GET \
    --get "http://localhost:8000/api/v1/templates/consequatur" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/api/v1/templates/consequatur"
);

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: 01KRHE7DN4JMZR465W2N17C9K9
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/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": "01KRHE7DN4JMZR465W2N17C9K9"
}
 

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: consequatur

template   integer     

Template ID. Example: 17

Template page preview

requires authentication

Returns a signed URL to a single page preview image (PNG). Phase 1 stuburl is null until Phase 3 wires Storage::temporaryUrl().

Scope: templates:read.

Example request:
curl --request GET \
    --get "http://localhost:8000/api/v1/templates/17/preview/17" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/api/v1/templates/17/preview/17"
);

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: 01KRHE7DNG28DQGAKXHX883JVR
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/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": "01KRHE7DNG28DQGAKXHX883JVR"
}
 

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: 17

page   integer     

Page number (1-indexed). Example: 17

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 "http://localhost:8000/api/v1/webhooks?active=&page=17&per_page=17" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"active\": false,
    \"page\": 73,
    \"per_page\": 13
}"
const url = new URL(
    "http://localhost:8000/api/v1/webhooks"
);

const params = {
    "active": "0",
    "page": "17",
    "per_page": "17",
};
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": false,
    "page": 73,
    "per_page": 13
};

fetch(url, {
    method: "GET",
    headers,
    body: JSON.stringify(body),
}).then(response => response.json());

Example response (401):

Show headers
x-request-id: 01KRHE7DQQPQXJC957HJXTSJPZ
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/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": "01KRHE7DQQPQXJC957HJXTSJPZ"
}
 

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: 17

per_page   integer  optional    

Max 100. Example: 17

Body Parameters

active   boolean  optional    

Example: false

page   integer  optional    

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

per_page   integer  optional    

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

Get webhook subscription

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/api/v1/webhooks/AYbBkm7h8gnApxq66c0TVGHFAb" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/api/v1/webhooks/AYbBkm7h8gnApxq66c0TVGHFAb"
);

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: 01KRHE7DR64B5HAJJ7AG41E6TN
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/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": "01KRHE7DR64B5HAJJ7AG41E6TN"
}
 

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: AYbBkm7h8gnApxq66c0TVGHFAb

List webhook deliveries

requires authentication

Example request:
curl --request GET \
    --get "http://localhost:8000/api/v1/webhook-deliveries?subscription_id=consequatur&event_name=consequatur&status=consequatur&page=17&per_page=17" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/api/v1/webhook-deliveries"
);

const params = {
    "subscription_id": "consequatur",
    "event_name": "consequatur",
    "status": "consequatur",
    "page": "17",
    "per_page": "17",
};
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: 01KRHE7DS0PCFV98AY90DA5RW1
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/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": "01KRHE7DS0PCFV98AY90DA5RW1"
}
 

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: consequatur

event_name   string  optional    

Filter by event name. Example: consequatur

status   string  optional    

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

page   integer  optional    

Example: 17

per_page   integer  optional    

Max 100. Example: 17

Get webhook delivery

requires authentication

Shows full payload, response code, error message, attempt history.

Example request:
curl --request GET \
    --get "http://localhost:8000/api/v1/webhook-deliveries/AYbBkm7h8gnApxq66c0TVGHFAb" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/api/v1/webhook-deliveries/AYbBkm7h8gnApxq66c0TVGHFAb"
);

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: 01KRHE7DS93WY28DHGGEMC5B1W
x-emza-api-version: v1
cache-control: no-cache, private
content-type: application/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": "01KRHE7DS93WY28DHGGEMC5B1W"
}
 

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: AYbBkm7h8gnApxq66c0TVGHFAb

Create webhook subscription

requires authentication

Registers an HTTPS endpoint to receive event deliveries. We POST each matching event to your URL with an HMAC-SHA256 signature in the X-Emza-Signature: t=<unix>,v1=<hex> header. Compute the signature over "{t}.{raw_body}" using the secret returned at creation.

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 \
    "http://localhost:8000/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(
    "http://localhost:8000/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 \
    "http://localhost:8000/api/v1/webhooks/01krh7z0e8a4n39gf2ary73xbj" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json" \
    --data "{
    \"url\": \"http:\\/\\/kunze.biz\\/iste-laborum-eius-est-dolor.html\",
    \"events\": [
        \"consequatur\"
    ],
    \"active\": false,
    \"rotate_secret\": false
}"
const url = new URL(
    "http://localhost:8000/api/v1/webhooks/01krh7z0e8a4n39gf2ary73xbj"
);

const headers = {
    "Authorization": "Bearer 42|abcdef1234567890abcdef1234567890abcdef",
    "Content-Type": "application/json",
    "Accept": "application/json",
};

let body = {
    "url": "http:\/\/kunze.biz\/iste-laborum-eius-est-dolor.html",
    "events": [
        "consequatur"
    ],
    "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://kunze.biz/iste-laborum-eius-est-dolor.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 \
    "http://localhost:8000/api/v1/webhooks/AYbBkm7h8gnApxq66c0TVGHFAb" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/api/v1/webhooks/AYbBkm7h8gnApxq66c0TVGHFAb"
);

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: AYbBkm7h8gnApxq66c0TVGHFAb

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 \
    "http://localhost:8000/api/v1/webhooks/AYbBkm7h8gnApxq66c0TVGHFAb/test" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/api/v1/webhooks/AYbBkm7h8gnApxq66c0TVGHFAb/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: AYbBkm7h8gnApxq66c0TVGHFAb

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 \
    "http://localhost:8000/api/v1/webhook-deliveries/AYbBkm7h8gnApxq66c0TVGHFAb/replay" \
    --header "Authorization: Bearer 42|abcdef1234567890abcdef1234567890abcdef" \
    --header "Content-Type: application/json" \
    --header "Accept: application/json"
const url = new URL(
    "http://localhost:8000/api/v1/webhook-deliveries/AYbBkm7h8gnApxq66c0TVGHFAb/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: AYbBkm7h8gnApxq66c0TVGHFAb