Developers
Send for signature from your own systems
Create envelopes from templates or uploaded PDFs, track them, and get a signed webhook when they finish. AI agents can do the same through MCP. Signing always stays with people.
Overview
The API uses the same rules as the EasySign app: the business you send as must be verified, your plan’s limits apply, and every action is recorded in the envelope’s audit trail with the name of the API key (and the agent, when the call comes through MCP).
- API keys are created by organisation owners and admins in the app underSettings → Developers. A key is shown once; store it like a password.
- Two environments: test keys start with
es_test_and work only on the development environment; live keys start withes_live_and work only in production. - All requests and responses are JSON over HTTPS, at
https://api.easysignit.com/v1.
Authentication
Send your key as a bearer token on every request:
Authorization: Bearer es_live_…An unknown or revoked key gets 401. Each key carries scopes; a call outside them gets 403 insufficient_scope. A key can also be limited to certain businesses.
| Scope | Allows |
|---|---|
documents:write | Upload PDFs (upload URL + create document). |
envelopes:read | List and read envelopes; get signed PDF and certificate links. |
envelopes:write | Create and void envelopes. |
templates:read | List templates. |
templates:send | Send an envelope from a template. |
webhooks:manage | Create, change and delete webhook endpoints. Not granted by default. |
Idempotency and limits
Add an Idempotency-Key header (up to 64 characters) to any POST. If a request is retried with the same key, you get the first response back instead of a second envelope. Keys are remembered for 24 hours; reusing one with a different body returns409 idempotency_conflict.
Each API key can make 120 requests per minute. Over that you get429 with a Retry-After header in seconds.
Upload a PDF
Uploading takes three calls: get a short-lived upload URL, PUT the file there, then create a document from it. PDFs that are password-protected, damaged or over 100 pages are rejected with 422 pdf_rejected and a reason.
# 1. Ask for an upload URL (valid 10 minutes, PDFs up to 20 MB)
curl -s -X POST https://api.easysignit.com/v1/documents/upload-url \
-H "Authorization: Bearer $EASYSIGN_KEY"
# -> {"upload_id": "...", "url": "https://...", "headers": {"Content-Type": "application/pdf"}, ...}
# 2. PUT the file to that URL with exactly the headers you were given
curl -s -X PUT "$UPLOAD_URL" -H "Content-Type: application/pdf" --data-binary @lease.pdf
# 3. Turn the upload into a document (it is checked and cleaned up first)
curl -s -X POST https://api.easysignit.com/v1/documents \
-H "Authorization: Bearer $EASYSIGN_KEY" \
-H "Content-Type: application/json" \
-d '{"upload_id": "'"$UPLOAD_ID"'", "name": "Lease.pdf"}'
# -> {"document": {"document_id": "doc_...", "pages": [{"width_pt": 612, "height_pt": 792, "rotation": 0}], ...}}Send from a template
Templates are made in the app (place the fields once, then “Save as template”). Each role in the template has an id; fill each role with a name and email.
curl -s -X POST https://api.easysignit.com/v1/templates/$TEMPLATE_ID/send \
-H "Authorization: Bearer $EASYSIGN_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4821" \
-d '{
"recipients": [{"id": "r1", "name": "Jordan Reyes", "email": "jordan@example.com"}],
"message": "Please review and sign by Friday.",
"reminder_days": 3,
"expires_in_days": 7
}'
# -> 201 {"envelope": {"id": "env_...", "status": "sent", ...}, "warnings": []}Send a PDF with fields
Recipients are signer, approver or cc. Recipients with the sameorder are emailed at the same time; higher orders wait until everyone before them has finished. Every signer needs at least one signature field.
Field positions are fractions of the page as it is displayed: x and y are the top-left corner from 0 to 1, w and h the size, and page starts at 0. Types: signature, initials, date_signed, name,text, checkbox, dropdown (with options).
curl -s -X POST https://api.easysignit.com/v1/envelopes \
-H "Authorization: Bearer $EASYSIGN_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: lease-2026-0193" \
-d '{
"kind": "pdf",
"document_id": "doc_...",
"title": "Studio lease",
"recipients": [
{"id": "r1", "role": "signer", "name": "Jordan Reyes", "email": "jordan@example.com", "order": 1},
{"id": "r2", "role": "approver", "name": "Sam Lee", "email": "sam@example.com", "order": 2},
{"id": "r3", "role": "cc", "name": "Front desk", "email": "desk@example.com", "order": 2}
],
"fields": [
{"id": "f1", "type": "signature", "recipient": "r1", "page": 1, "x": 0.12, "y": 0.80, "w": 0.30, "h": 0.06},
{"id": "f2", "type": "date_signed", "recipient": "r1", "page": 1, "x": 0.55, "y": 0.81, "w": 0.20, "h": 0.04},
{"id": "f3", "type": "text", "recipient": "r1", "page": 0, "x": 0.12, "y": 0.30, "w": 0.40, "h": 0.04, "label": "Company"}
]
}'Check status and download
Envelope status is sent (in progress), completed,declined, voided or expired. Each recipient has its own state. Prefer webhooks to polling.
# Status and recipients
curl -s https://api.easysignit.com/v1/envelopes/env_... -H "Authorization: Bearer $EASYSIGN_KEY"
# -> {"envelope": {"id": "env_...", "status": "completed", "recipients": [...], ...}}
# Short-lived (5 minute) links to the sealed files once status is "completed"
curl -s https://api.easysignit.com/v1/envelopes/env_.../signed-pdf -H "Authorization: Bearer $EASYSIGN_KEY"
curl -s https://api.easysignit.com/v1/envelopes/env_.../certificate -H "Authorization: Bearer $EASYSIGN_KEY"
# -> {"url": "https://..."}Webhooks
Add endpoints in Settings → Developers or with /v1/webhooks (scopewebhooks:manage). Endpoints must use HTTPS on a public address. Each endpoint gets a signing secret starting whsec_, shown once.
| Event | Sent when |
|---|---|
envelope.sent | The envelope was created and the first recipients were emailed. |
envelope.viewed | A recipient opened their signing link. |
recipient.completed | A recipient signed or approved. |
recipient.declined | A recipient declined to sign. |
envelope.completed | Everyone finished; the sealed PDF and certificate are ready. |
envelope.declined | The envelope ended because someone declined. |
envelope.voided | The sender voided the envelope. |
envelope.expired | The envelope expired before everyone finished. |
recipient.bounced | An email to a recipient bounced or was marked as spam. |
Each delivery is a POST with a JSON body like this:
{
"id": "evt_...",
"type": "envelope.completed",
"created_at": "2026-10-15T19:57:28Z",
"data": {
"envelope": { "id": "env_...", "title": "Studio lease", "status": "completed", "recipients": [ ... ] }
}
}Reply with any 2xx within 10 seconds. Anything else, a timeout or a redirect counts as a failure and is retried after about 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours (7 attempts over roughly 21 hours). Use EasySign-Event-Id to ignore duplicates. After 20 failures in a row the endpoint is turned off and your owners and admins are emailed; turn it back on in the app once it’s fixed. The app shows the last deliveries for each endpoint and can resend one or send a test event.
Verifying webhook signatures
Every delivery carries these headers:
EasySign-Event-Id: evt_...
EasySign-Timestamp: 1792094248
EasySign-Signature: t=1792094248,v1=5f2c...e81av1 is the hex HMAC-SHA256 of {t}.{raw body} using your endpoint’s secret. Verify it against the raw body (before parsing JSON), compare in constant time, and reject timestamps more than 5 minutes old.
Node.js
import crypto from 'node:crypto';
// rawBody: the request body exactly as received (string or Buffer), before JSON parsing.
export function verifyEasySign(secret, signatureHeader, rawBody, toleranceSec = 300) {
const parts = {};
for (const item of signatureHeader.split(',')) {
const i = item.indexOf('=');
if (i > 0) parts[item.slice(0, i).trim()] = item.slice(i + 1).trim();
}
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(parts.v1 || '', 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Python
import hashlib
import hmac
import time
def verify_easysign(secret: str, signature_header: str, raw_body: bytes, tolerance: int = 300) -> bool:
"""raw_body: the request body bytes exactly as received, before JSON parsing."""
parts = dict(item.strip().split("=", 1) for item in signature_header.split(",") if "=" in item)
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))AI agents (MCP)
EasySign runs a remote Model Context Protocol server athttps://api.easysignit.com/mcp (streamable HTTP). Connect an agent with an API key; the key’s scopes and business limits apply, and every action shows in the audit trail with the key and the agent’s name.
Agents can
- List templates and send from them.
- Upload a PDF and send it with fields.
- Check status, void, and fetch the signed PDF and certificate.
Agents can’t
- Sign, approve or consent for anyone. There is no tool for it.
- Act outside the key’s scopes or businesses.
Claude Code
claude mcp add --transport http easysign https://api.easysignit.com/mcp \
--header "Authorization: Bearer es_live_…"Or in a project’s .mcp.json (other clients that support remote HTTP servers take the same URL and header):
{
"mcpServers": {
"easysign": {
"type": "http",
"url": "https://api.easysignit.com/mcp",
"headers": { "Authorization": "Bearer es_live_…" }
}
}
}| Tool | What it does |
|---|---|
list_templates | Your organisation’s templates and their roles. |
send_template | Send an envelope from a template to named recipients. |
get_upload_url / create_document_from_upload | Upload a PDF in two steps. |
send_pdf | Send an uploaded PDF with recipients and fields. |
list_envelopes / get_envelope | Find envelopes and read their status. |
void_envelope | Cancel an envelope that is still in progress. |
get_signed_pdf_url / get_certificate_url | Short-lived links to the sealed files. |
API reference
The full reference is an OpenAPI 3.1 document:https://api.easysignit.com/v1/openapi.json. Import it into your API client or generate a typed client from it.