Easy Sign API
Automate signature requests and verify documents with a single API call. Perfect for scripts, backend services, and integrations.
Quick Start
- 01
Create an API Key
Go to Settings → API Keys and create a new key. API access requires an active Pro subscription.
- 02
Prepare Your Document
Have a PDF ready (URL or base64 encoded). URL mode supports up to 20MB (Pro); base64 mode up to 3MB. Up to 15 pages.
- 03
Send a Request
Call the API endpoint to create and send a signature request.
Authentication
API access requires an active Pro subscription. All API requests require an API key passed in the Authorization header:
Authorization: Bearer es_live_your_api_key_here
Rate Limits & Retries
Rate Limit Headers
When you exceed your per-key rate limit, the API returns HTTP 429 with these headers so your client can back off correctly:
| Header | Description |
|---|---|
| Retry-After | Seconds to wait before retrying (rounded up from the reset time) |
| X-RateLimit-Remaining | Requests remaining in the current window (always 0 on a 429) |
| X-RateLimit-Reset | Unix epoch milliseconds when the rate-limit window resets |
Successful (2xx) responses currently do not include rate-limit headers — only the 429 response carries them. Plan your retry policy around Retry-After.
Idempotency & Retries
Important. POST /api/agent/sign-request has external side effects: it sends a real email to the recipient. Retrying a failed-but-actually-sent request will create a duplicate envelope and send a second email.
To make retries safe, send an Idempotency-Key header (any unique string, e.g. a UUID, max 255 chars):
-H "Idempotency-Key: 5f3a...-unique-per-attempt"
- Retrying with the same key replays the original result (same envelope, no second email) and returns
Idempotent-Replay: true. - Deduplication is keyed on the header only — it does not compare request bodies. A repeated key with a different payload still replays the first result.
- A concurrent retry while the first is still processing returns
409 IDEMPOTENCY_IN_PROGRESS— wait and check your dashboard before retrying again. - For 429, honor
Retry-After.
Send Signature Request
/api/agent/sign-requestCreate and send a signature request in a single call.
Write-only. This API creates and sends; there is no endpoint to read signing status back with an API key. To detect completion, use the Verify API with the transaction_id printed on the completed document, or track envelopes in your dashboard.
Request Example (cURL)
curl -X POST https://www.easy-sign.ca/api/agent/sign-request \
-H "Authorization: Bearer es_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"recipient": {
"email": "client@example.com",
"name": "Jane Smith"
},
"document": {
"url": "https://example.com/contract.pdf"
},
"fields": [
{
"type": "signature",
"page": 1,
"preset": "signature_bottom_right"
},
{
"type": "date",
"page": 1,
"preset": "date_after_signature"
}
],
"envelope_name": "Contract Agreement"
}'Request Parameters
The sender identity is derived automatically from the account that owns the API key (your display name and account email). Any sender field in the request body is ignored.
| Parameter | Type | Description |
|---|---|---|
| recipient.email | string | Recipient's email address (required) |
| recipient.name | string | Recipient's name (optional) |
| document.url | string | Public URL to PDF document |
| document.base64 | string | Base64 encoded PDF content |
| document.file_name | string | File name (optional, auto-detected) |
| fields | array | Array of signature fields (required) |
| envelope_name | string | Custom name for the envelope (optional) |
| sender_timezone | string | IANA timezone name used for the timestamp stamped next to the signature, e.g. America/Toronto (optional, defaults to UTC). Invalid values fall back to UTC. Fresh responses echo the zone that was actually applied, so you can assert on it; a response replayed from an Idempotency-Key returns the body cached at first execution, which may predate this field. |
Field Configuration
Each field requires a type, page number, and position (using preset or custom coordinates).
Field Types
signature- Signature fieldsignature_date- Signature with datedate- Date onlysigned_datetime- Date and time the recipient signed, filled by the server and read-only for both parties
Position Presets
signature_bottom_rightsignature_bottom_leftsignature_bottom_centerdate_after_signaturesignature_footer
For custom positioning, use the position object:
{
"type": "signature",
"page": 1,
"position": {
"x": 60, // X position (0-100%)
"y": 85, // Y position (0-100%)
"width": 25, // Width (1-50%)
"height": 8 // Height (1-20%)
}
}Success Response (HTTP 201 Created)
{
"success": true,
"envelope_id": "550e8400-e29b-41d4-a716-446655440000",
"signing_url": "https://www.easy-sign.ca/sign/550e8400...?token=abc123",
"expires_at": "<ISO 8601 timestamp, ~7 days from request time>",
"recipient_email": "client@example.com"
}A successful request returns HTTP 201. If non-critical post-send updates fail (e.g. envelope status flip), the response may include an optional warnings: string[] array — the email has already been sent and cannot be recalled, so treat warnings as advisory.
Error Response
{
"success": false,
"error": {
"code": "INVALID_EMAIL",
"message": "Valid recipient email is required"
}
}Verify Document
/api/verifyVerify the authenticity and integrity of signed documents. No authentication required (public API). Rate limited to 20 requests/minute per IP; exceeding it returns HTTP 429 with a Retry-After header.
What each field means
verified- The transaction record exists and has not been voided. It expresses no conclusion whatsoever about an uploaded file — readverificationType/hashComparisonfor that.transaction_only- The Transaction ID exists in our records, but this file's integrity was not confirmed. Returned when no file was sent, when no hash is on record, and when the uploaded file matches none of the hashes on record.full- A file was uploaded and its SHA-256 hash matched one on record. This is the only value that asserts file integrity.hashComparison- Present on every response that found a transaction record; tells the three casestransaction_onlycollapses together apart:not_compared(no file sent, or no hash on record),match(always paired withfull), andunrecognized(a file was compared and matched nothing on record). Branch on this rather than parsingmessage.
Request Example (JSON)
curl -X POST https://www.easy-sign.ca/api/verify \
-H "Content-Type: application/json" \
-d '{ "transactionId": "ES-20241206-ABC12345" }'Request Example (with File Verification)
curl -X POST https://www.easy-sign.ca/api/verify \ -F "file=@signed-document.pdf" \ -F "transactionId=ES-20241206-ABC12345"
Request Parameters
| Parameter | Type | Description |
|---|---|---|
| transactionId | string | Transaction ID (format: ES-YYYYMMDD-XXXXXXXX). Found on signed documents. |
| file | File | Optional: Upload the signed PDF for full integrity verification |
Success Response (Full Verification)
{
"verified": true,
"transactionId": "ES-20241206-ABC12345",
"status": "valid",
"verificationType": "full",
"hashComparison": "match",
"message": "Document verified successfully. File integrity confirmed - no modifications detected.",
"details": {
"envelopeName": "Contract Agreement",
"status": "completed",
"createdAt": "2024-12-06T10:00:00Z",
"completedAt": "2024-12-06T11:30:00Z",
"recipientCount": 1,
"signedCount": 1,
"documentCount": 1
}
}Status Values
| Status | Description |
|---|---|
| valid | Document is fully signed. Read verificationType to know whether file integrity was also confirmed — full means it was, transaction_only means it was not. |
| pending | Document exists but signing is not yet complete. |
| not_found | Transaction ID does not exist in our records. |
| voided | Document was voided by the sender after creation and is no longer legally valid. Returned with verified: false. |
Unrecognized File Response
If the uploaded file matches none of the hashes on record, the endpoint does not declare the file altered. A hash mismatch only shows that the file is not one we hold a hash for — which is the normal case for the audit certificate, for re-saved, printed or scanned copies, and for documents signed before hashes were recorded. The transaction record is returned with verificationType = transaction_only and hashComparison = unrecognized, meaning integrity was neither confirmed nor denied. A match is the only positive confirmation we can give: a file we hold no hash for and a file that was altered are indistinguishable to us, so treat any non-match as unknown — never as verified, and never as forged.
{
"verified": true,
"transactionId": "ES-20241206-ABC12345",
"status": "valid",
"verificationType": "transaction_only",
"hashComparison": "unrecognized",
"message": "Transaction record found, but the uploaded file is not one of the 1 document(s) we hold a hash for on this transaction, so its integrity can be neither confirmed nor denied. This is expected for the audit certificate, for a re-saved, printed or scanned copy, and for documents signed before hashes were recorded. A hash match is the only positive confirmation we can give: a file we hold no hash for and a file that was altered are indistinguishable to us. The transaction details below come from our records, not from the file you provided.",
"details": {
"envelopeName": "Contract Agreement",
"status": "completed",
"createdAt": "2024-12-06T10:00:00Z",
"completedAt": "2024-12-06T11:30:00Z",
"recipientCount": 1,
"signedCount": 1,
"documentCount": 1
}
}GET /api/verify (Discovery)
A GET request to the same path returns endpoint metadata — useful for tooling that auto-discovers APIs.
{
"name": "Easy Sign Document Verification API",
"version": "1.0",
"description": "Verify the authenticity of documents signed via Easy Sign",
"usage": {
"method": "POST",
"contentType": "application/json or multipart/form-data",
"parameters": {
"transactionId": "The Transaction ID found on the document (format: ES-YYYYMMDD-XXXXXXXX)",
"file": "Optional: Upload the signed PDF file for verification"
}
},
"legal": {
"notice": "Documents signed via Easy Sign are legally binding under Canadian provincial e-commerce legislation (e.g., Ontario Electronic Commerce Act, 2000), as well as ESIGN/UETA (US) and eIDAS (EU).",
"auditTrail": "Complete audit trails are maintained for all signed documents."
}
}Integrations & Automation
Wire the API into command-line tools like Claude Code, MCP clients, or your own scripts to send signature requests on your behalf. Ideal for automating document workflows from the tooling you already use.
Claude Code Skill
Save the following as .claude/commands/easy-sign.md in your project or home directory:
---
name: send-signature-request
description: Send a document for electronic signature via Easy Sign API
---
# Send Signature Request
Use this skill when the user wants to send a PDF document for electronic signature.
## Prerequisites
- Easy Sign API key stored in $EASY_SIGN_API_KEY
- PDF document (URL or local file path)
## Usage
Call the Easy Sign API to send a signature request:
```bash
curl -X POST https://www.easy-sign.ca/api/agent/sign-request \
-H "Authorization: Bearer $EASY_SIGN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipient": { "email": "RECIPIENT_EMAIL", "name": "RECIPIENT_NAME" },
"document": { "url": "DOCUMENT_URL" },
"fields": [
{ "type": "signature", "page": 1, "preset": "signature_bottom_right" },
{ "type": "date", "page": 1, "preset": "date_after_signature" }
],
"envelope_name": "Document Name"
}'
```
## Response Handling
On success, return the `signing_url` to the user.
The recipient will receive an email notification.
Link expires in 7 days.MCP Tool Definition
Use this JSON schema to define an MCP tool for Easy Sign integration:
{
"name": "easy_sign_send_signature_request",
"description": "Send a PDF document for electronic signature. The recipient receives an email with a signing link.",
"inputSchema": {
"type": "object",
"properties": {
"recipient": {
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" },
"name": { "type": "string" }
},
"required": ["email"]
},
"document": {
"type": "object",
"description": "Provide either url (max 20MB) or base64 (max 3MB), up to 15 pages",
"properties": {
"url": { "type": "string", "format": "uri" },
"base64": { "type": "string" },
"file_name": { "type": "string" }
}
},
"fields": {
"type": "array",
"minItems": 1,
"description": "Signature fields. At least one is required. Each field must have either a preset or a position.",
"items": {
"type": "object",
"properties": {
"type": { "type": "string", "enum": ["signature", "signature_date", "date", "signed_datetime"] },
"page": { "type": "integer", "minimum": 1 },
"preset": {
"type": "string",
"enum": [
"signature_bottom_right",
"signature_bottom_left",
"signature_bottom_center",
"signature_footer",
"date_after_signature"
]
},
"position": {
"type": "object",
"description": "Custom placement as percent of page. Use instead of preset.",
"properties": {
"x": { "type": "number", "minimum": 0, "maximum": 100 },
"y": { "type": "number", "minimum": 0, "maximum": 100 },
"width": { "type": "number", "minimum": 1, "maximum": 50 },
"height": { "type": "number", "minimum": 1, "maximum": 20 }
},
"required": ["x", "y", "width", "height"]
},
"required": { "type": "boolean", "description": "Whether the field is mandatory (default true)" }
},
"required": ["type", "page"],
"anyOf": [
{ "required": ["preset"] },
{ "required": ["position"] }
]
}
},
"envelope_name": { "type": "string", "description": "Custom envelope name (optional)" },
"sender_timezone": { "type": "string", "description": "IANA timezone name for the timestamp stamped next to the signature, e.g. America/Toronto (optional, defaults to UTC). Invalid values fall back to UTC; fresh responses echo the zone actually applied (replayed idempotent responses return the originally cached body)." }
},
"required": ["recipient", "document", "fields"]
}
}Environment Setup
Store your API key securely in an environment variable:
# Add to your shell profile (~/.bashrc, ~/.zshrc, etc.) export EASY_SIGN_API_KEY="es_live_your_api_key_here"
Example Workflow
Here's how an integrated signing flow might look from a tool that calls the API:
User
Send the contract at https://example.com/contract.pdf to john@example.com for signature. I'm jane@company.com.
Assistant
I'll send that document for signature using Easy Sign...
Signature request sent successfully!
John (john@example.com) will receive an email with a link to sign the document. The link expires in 7 days.
Best Practices
- Confirm document and recipient details before sending
- Store API keys in environment variables, never hardcode
- Handle errors gracefully with clear feedback to users
- Use meaningful envelope names for easy tracking
Error Codes
Sign Request API
| Code | HTTP | Description |
|---|---|---|
| INVALID_FORMAT | 401 | Authorization value present but not a valid key (must start with es_live_) |
| INVALID_API_KEY | 401 | Authorization header missing or malformed, or key not recognized |
| KEY_REVOKED | 401 | API key has been revoked by the owner |
| KEY_EXPIRED | 401 | API key has expired |
| PRO_REQUIRED | 401 | API access requires an active Pro subscription |
| DATABASE_ERROR | 401 | Temporary server error while validating the key — safe to retry |
| RATE_LIMITED | 429 | Too many requests (10/minute per key; contact us to raise) |
| INVALID_IDEMPOTENCY_KEY | 400 | Idempotency-Key exceeds 255 characters |
| IDEMPOTENCY_IN_PROGRESS | 409 | A request with the same Idempotency-Key is still processing |
| INVALID_JSON | 400 | Request body is not valid JSON |
| INVALID_EMAIL | 400 | Email format is invalid |
| SAME_EMAIL | 400 | Sender and recipient are the same |
| DOCUMENT_TOO_LARGE | 400 | Document exceeds the 20 MB (Pro plan) limit, or base64 exceeds 3 MB |
| DOCUMENT_TOO_MANY_PAGES | 400 | Document exceeds 15 page limit |
| INVALID_PDF | 400 / 500 | Invalid or corrupted PDF (400). Returned as 500 if server-side PDF tooling is temporarily unavailable — safe to retry. |
| INVALID_URL | 400 | Document URL is malformed or unsafe (SSRF protection) |
| DOCUMENT_URL_FAILED | 400 | Failed to download from URL |
| INVALID_FILE_NAME | 400 | File name exceeds 255 characters |
| NO_FIELDS | 400 | No signature fields provided |
| INVALID_FIELD_TYPE | 400 | Field type must be signature, signature_date, date, or signed_datetime |
| INVALID_FIELD_POSITION | 400 | Field position is invalid |
| STORAGE_EXCEEDED | 400 | Total storage limit reached for your plan |
| EMAIL_FAILED | 500 | Outbound signature email failed to send (request rolled back) |
| INTERNAL_ERROR | 500 | Unexpected server error — safe to retry after a short delay |
Verify API
The Verify API does not use the error.code envelope. Outcomes are conveyed via the status field plus the HTTP status code.
| Status | HTTP | Description |
|---|---|---|
| valid | 200 | Document signed. Integrity is confirmed only when verificationType is full; an uploaded file that matches no hash on record still returns valid + transaction_only. |
| pending | 200 | Document exists but signing is incomplete |
| voided | 200 | Sender voided the document — no longer legally valid |
| not_found | 400 | Transaction ID format invalid (must be ES-YYYYMMDD-XXXXXXXX) |
| not_found | 404 | Transaction ID does not exist or refers to a deleted record |
| not_found | 500 | Internal server error. The body still carries status: 'not_found' for legacy reasons — treat this as a transient backend failure and retry with jittered exponential backoff. |
Limits
20 MB
Max file size via URL (Pro plan); base64 mode up to 3 MB
15
Max pages per document
1
Recipient per request
10/min
Rate limit per API key (contact us to raise)
7 days
Signing link validity
5
API keys per user
Need Help?
Have questions or need assistance? We're here to help.