e-Sign — Create Signing Request
esignSend a PDF for legally-valid electronic signature under the IT Act. PRICING IS PER SIGNATORY, not per request. One call can carry several signers, and each is a separate signing transaction — its own request_id, its own invitation, its own Aadhaar/DSC authentication. The price shown is the rate for ONE signatory; a request is billed at that rate × the number of entries in `signers`. A two-party NDA therefore costs twice the listed rate. Your wallet must cover the full amount before the request is created, or the call returns 402 INSUFFICIENT_BALANCE and nothing is sent. Each signer gets their own `signing_url` and authenticates by Aadhaar OTP, WhatsApp or auto-DSC (`signer_auth_type`). You receive a `group_id` for the whole envelope and a per-signer `request_id`. INVITES ARE SENT FOR YOU. Each signer is emailed/messaged a signing invitation automatically, and the response reports `invitation_status` per signer. You also receive a `signing_url` per signer, so you can additionally surface the link in your own UI if you prefer — but you do not have to deliver it for signing to happen. COMPLETION. Signing happens after this call returns, so the response tells you the request was created, NOT that anything is signed. Track completion by polling, or register a callback and treat it as a prompt to re-check rather than as proof — never release a document on the strength of an unverified callback alone. `sign_coordinates` places the signature block on the page (x/y/page_num). Omit it to use the provider default position.
Authentication
Pass your key in the X-API-Key header. Use a test_ key against the sandbox and a live_ key in production. Send an optional Idempotency-Key header to safely retry — the same key returns the same response for 24h.
Request
Endpoint: POST https://apisathi.in/gw/v1/esign-create-v1/
The trailing slash is required. /v1/esign-create-v1/ works; /v1/esign-create-v1 returns 404 Not Found. This applies to every product.
| Field | Type | Required | Constraints |
|---|---|---|---|
| document | object | required | The PDF to be signed. |
| signers | object[] | required | One entry per signer; each receives their own signing_url. |
Code snippets
curl -X POST https://apisathi.in/gw/v1/esign-create-v1/ \
-H "X-API-Key: $API_SATHI_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"document":{"name":"nda.pdf","data":"<base64-encoded PDF>"},"signers":[{"signer_name":"Rahul Verma","signer_email":"rahul@example.com","signer_phone_number":"+919876543210","signer_auth_type":"AADHAAR","signer_purpose":"Borrower","sign_coordinates":[{"x_coord":100,"y_coord":100,"page_num":1}]}]}'Response
| Field | Type | Required | Constraints |
|---|---|---|---|
| group_id | string | optional | Identifies the whole signing envelope. |
| requests | array | optional | Per-signer request_id and signing_url. Deliver these links yourself. |
| expires_at | string | optional | — |
| webhook_security_key | string | optional | — |
Sample response
{
"group_id": "6a79b62d04efdfca72af83b7",
"requests": [
{
"request_id": "6a79b62d04efdfca72af83b8",
"signer_name": "Rahul Verma",
"signer_email": "rahul@example.com",
"signer_phone_number": "+919876543210",
"signer_auth_type": "AADHAAR",
"signer_auth_mode": "OTP",
"signing_order": 1,
"invitation_status": "INVITATION_SUCCESS",
"signing_url": "https://esign.example.com/v5/viewer/6a79b62d04efdfca72af83b8"
}
],
"expires_at": "2026-08-17T11:29:49.065+00:00",
"webhook_security_key": "6a56499c-0000-0000-0000-000000000000"
}Reading a 400
A schema rejection returns INVALID_REQUEST with a details array naming the exact field. Read it before guessing — it gives you the JSON path and what was wrong with it.
{
"error": {
"code": "INVALID_REQUEST",
"message": "Request body failed schema validation",
"details": [
{ "path": "/callback_url",
"message": "unexpected property 'callback_url' — this endpoint does not accept unknown fields" }
]
}
}Most endpoints reject unknown top-level fields outright, so an extra key you added for your own bookkeeping will fail the call. Enum errors list the accepted values, and a missing required field is named with its full path.
Error codes
| Code | HTTP | When |
|---|---|---|
| INVALID_INPUT | 422 | The request was rejected as invalid — either it failed OUR schema validation (malformed input; not charged), or the upstream source rejected the value you sent. NOTE: an identifier that is well-formed but simply has NO RECORD is no longer an error — it returns 200 with `verified: false` and is charged (see the result-code table below). FIX the input before retrying; retrying the same value will fail again. |
| INVALID_API_KEY | 401 | Missing, malformed, or revoked X-API-Key. |
| OUT_OF_SCOPE | 403 | API key is not scoped for this product. |
| INSUFFICIENT_BALANCE | 402 | Wallet balance is below the per-call sale price. Recharge and retry. |
| RATE_LIMITED | 429 | Per-key RPS or RPM limit exceeded. Back off and retry after the Retry-After header. |
| PRODUCT_DEPRECATED | 410 | This API has been retired and is no longer available. Stop calling it — it will not return. Check the catalog for the current equivalent. |
| ROUTER_NO_VENDOR | 503 | No healthy vendor is currently available for this product. Transient — safe to retry after a short backoff. Not charged. |
| VENDOR_AUTH_FAILED | 502 | Upstream vendor rejected our credentials (our config issue). Not charged. |
| VENDOR_ERROR | 502 | A genuine transient upstream error (the source was briefly unavailable). Safe to RETRY after a short backoff. Not charged. NOTE: this is NOT for bad input — invalid values return 422 INVALID_INPUT, not 502. |
| TIMEOUT | 504 | Upstream vendor did not respond within the SLA window. Safe to retry after a short backoff. Not charged. |
OpenAPI 3.1
Generated from this product's request/response JSON Schemas.
{
"openapi": "3.1.0",
"info": {
"title": "API Sathi — e-Sign — Create Signing Request",
"version": "1.0.0",
"description": "Send a PDF for legally-valid electronic signature under the IT Act.\n\nPRICING IS PER SIGNATORY, not per request. One call can carry several signers, and each is a separate signing transaction — its own request_id, its own invitation, its own Aadhaar/DSC authentication. The price shown is the rate for ONE signatory; a request is billed at that rate × the number of entries in `signers`. A two-party NDA therefore costs twice the listed rate. Your wallet must cover the full amount before the request is created, or the call returns 402 INSUFFICIENT_BALANCE and nothing is sent.\n\nEach signer gets their own `signing_url` and authenticates by Aadhaar OTP, WhatsApp or auto-DSC (`signer_auth_type`). You receive a `group_id` for the whole envelope and a per-signer `request_id`.\n\nINVITES ARE SENT FOR YOU. Each signer is emailed/messaged a signing invitation automatically, and the response reports `invitation_status` per signer. You also receive a `signing_url` per signer, so you can additionally surface the link in your own UI if you prefer — but you do not have to deliver it for signing to happen.\n\nCOMPLETION. Signing happens after this call returns, so the response tells you the request was created, NOT that anything is signed. Track completion by polling, or register a callback and treat it as a prompt to re-check rather than as proof — never release a document on the strength of an unverified callback alone.\n\n`sign_coordinates` places the signature block on the page (x/y/page_num). Omit it to use the provider default position."
},
"servers": [
{
"url": "https://apisathi.in/gw/v1"
}
],
"components": {
"securitySchemes": {
"ApiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "X-API-Key",
"description": "Your live or test key, e.g. `live_xxxxxxxxxxxx`."
}
}
},
"paths": {
"/esign-create-v1": {
"post": {
"operationId": "esignCreateV1",
"tags": [
"esign"
],
"summary": "e-Sign — Create Signing Request",
"description": "Send a PDF for legally-valid electronic signature under the IT Act.\n\nPRICING IS PER SIGNATORY, not per request. One call can carry several signers, and each is a separate signing transaction — its own request_id, its own invitation, its own Aadhaar/DSC authentication. The price shown is the rate for ONE signatory; a request is billed at that rate × the number of entries in `signers`. A two-party NDA therefore costs twice the listed rate. Your wallet must cover the full amount before the request is created, or the call returns 402 INSUFFICIENT_BALANCE and nothing is sent.\n\nEach signer gets their own `signing_url` and authenticates by Aadhaar OTP, WhatsApp or auto-DSC (`signer_auth_type`). You receive a `group_id` for the whole envelope and a per-signer `request_id`.\n\nINVITES ARE SENT FOR YOU. Each signer is emailed/messaged a signing invitation automatically, and the response reports `invitation_status` per signer. You also receive a `signing_url` per signer, so you can additionally surface the link in your own UI if you prefer — but you do not have to deliver it for signing to happen.\n\nCOMPLETION. Signing happens after this call returns, so the response tells you the request was created, NOT that anything is signed. Track completion by polling, or register a callback and treat it as a prompt to re-check rather than as proof — never release a document on the strength of an unverified callback alone.\n\n`sign_coordinates` places the signature block on the page (x/y/page_num). Omit it to use the provider default position.",
"security": [
{
"ApiKeyAuth": []
}
],
"parameters": [
{
"name": "Idempotency-Key",
"in": "header",
"required": false,
"schema": {
"type": "string"
},
"description": "Optional. Same key returns the same response for 24h."
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"document",
"signers"
],
"properties": {
"document": {
"type": "object",
"description": "The PDF to be signed.",
"required": [
"name",
"data"
],
"properties": {
"name": {
"type": "string",
"description": "File name, e.g. nda.pdf"
},
"data": {
"type": "string",
"description": "Base64-encoded PDF."
}
}
},
"signers": {
"type": "array",
"description": "One entry per signer; each receives their own signing_url.",
"items": {
"type": "object",
"required": [
"signer_name",
"signer_email",
"signer_phone_number",
"signer_auth_type"
],
"properties": {
"signer_name": {
"type": "string"
},
"signer_email": {
"type": "string"
},
"signer_phone_number": {
"type": "string",
"description": "With country code, e.g. +919876543210."
},
"signer_auth_type": {
"type": "string",
"enum": [
"AADHAAR",
"WHATSAPP",
"AUTO_DSC"
],
"description": "How the signer proves identity."
},
"signer_city": {
"type": "string"
},
"signer_purpose": {
"type": "string"
},
"sign_coordinates": {
"type": "array",
"description": "REQUIRED. Where the signature block is stamped: { x_coord, y_coord, page_num }. Omitting it is rejected.",
"items": {
"type": "object",
"properties": {
"x_coord": {
"type": "number"
},
"y_coord": {
"type": "number"
},
"page_num": {
"type": "number"
}
}
}
}
}
}
}
}
},
"example": {
"document": {
"name": "nda.pdf",
"data": "<base64-encoded PDF>"
},
"signers": [
{
"signer_name": "Rahul Verma",
"signer_email": "rahul@example.com",
"signer_phone_number": "+919876543210",
"signer_auth_type": "AADHAAR",
"signer_purpose": "Borrower",
"sign_coordinates": [
{
"x_coord": 100,
"y_coord": 100,
"page_num": 1
}
]
}
]
}
}
}
},
"responses": {
"200": {
"description": "Successful, normalized response.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"group_id": {
"type": "string",
"description": "Identifies the whole signing envelope."
},
"requests": {
"type": "array",
"description": "Per-signer request_id and signing_url. Deliver these links yourself."
},
"expires_at": {
"type": "string"
},
"webhook_security_key": {
"type": "string"
}
},
"additionalProperties": true
},
"example": {
"group_id": "6a79b62d04efdfca72af83b7",
"requests": [
{
"request_id": "6a79b62d04efdfca72af83b8",
"signer_name": "Rahul Verma",
"signer_email": "rahul@example.com",
"signer_phone_number": "+919876543210",
"signer_auth_type": "AADHAAR",
"signer_auth_mode": "OTP",
"signing_order": 1,
"invitation_status": "INVITATION_SUCCESS",
"signing_url": "https://esign.example.com/v5/viewer/6a79b62d04efdfca72af83b8"
}
],
"expires_at": "2026-08-17T11:29:49.065+00:00",
"webhook_security_key": "6a56499c-0000-0000-0000-000000000000"
}
}
}
},
"401": {
"description": "Missing, malformed, or revoked X-API-Key.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
},
"call_id": {
"type": "string"
}
}
}
}
}
}
}
},
"402": {
"description": "Wallet balance is below the per-call sale price. Recharge and retry.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
},
"call_id": {
"type": "string"
}
}
}
}
}
}
}
},
"403": {
"description": "API key is not scoped for this product.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
},
"call_id": {
"type": "string"
}
}
}
}
}
}
}
},
"410": {
"description": "This API has been retired and is no longer available. Stop calling it — it will not return. Check the catalog for the current equivalent.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
},
"call_id": {
"type": "string"
}
}
}
}
}
}
}
},
"422": {
"description": "The request was rejected as invalid — either it failed OUR schema validation (malformed input; not charged), or the upstream source rejected the value you sent. NOTE: an identifier that is well-formed but simply has NO RECORD is no longer an error — it returns 200 with `verified: false` and is charged (see the result-code table below). FIX the input before retrying; retrying the same value will fail again.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
},
"call_id": {
"type": "string"
}
}
}
}
}
}
}
},
"429": {
"description": "Per-key RPS or RPM limit exceeded. Back off and retry after the Retry-After header.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
},
"call_id": {
"type": "string"
}
}
}
}
}
}
}
},
"502": {
"description": "Upstream vendor rejected our credentials (our config issue). Not charged.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
},
"call_id": {
"type": "string"
}
}
}
}
}
}
}
},
"503": {
"description": "No healthy vendor is currently available for this product. Transient — safe to retry after a short backoff. Not charged.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
},
"call_id": {
"type": "string"
}
}
}
}
}
}
}
},
"504": {
"description": "Upstream vendor did not respond within the SLA window. Safe to retry after a short backoff. Not charged.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "object",
"properties": {
"code": {
"type": "string"
},
"message": {
"type": "string"
},
"call_id": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
}
}
}