Skip to main content
API · Reference

Easy Sign API

Automate signature requests and verify documents with a single API call. Perfect for scripts, backend services, and integrations.

Quick Start

  1. 01

    Create an API Key

    Go to Settings → API Keys and create a new key. API access requires an active Pro subscription.

  2. 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.

  3. 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:

HeaderDescription
Retry-AfterSeconds to wait before retrying (rounded up from the reset time)
X-RateLimit-RemainingRequests remaining in the current window (always 0 on a 429)
X-RateLimit-ResetUnix 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-send"
  • 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

POST/api/agent/sign-request

Create 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)

Request
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.

ParameterTypeDescription
recipient.emailstringRecipient's email address (required)
recipient.namestringRecipient's name (optional)
recipient.localestringLanguage of the invitation email and signing page, one of en, fr, zh-hans (optional, defaults to en)
document.urlstringPublic URL to PDF document
document.base64stringBase64 encoded PDF content
document.file_namestringFile name (optional, auto-detected)
fieldsarrayArray of signature fields (required)
envelope_namestringCustom name for the envelope (optional)
sender_timezonestringIANA 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). Fields are mandatory for the signer by default; set "required": false to make one optional.

Field Types

  • signature - Signature field
  • signature_date - Signature with date
  • date - The date the request is sent, filled in automatically
  • signed_datetime - Date and time the recipient signed, filled by the server and read-only for both parties

Position Presets

  • signature_bottom_right
  • signature_bottom_left
  • signature_bottom_center
  • date_after_signature
  • signature_footer

For custom positioning, use the position object. Values are percent of the page, measured from its top-left corner:

{
  "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)

Response
{
  "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 by default, up to 30 on Pro>",
  "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.

Keep signing_url to yourself. It is the signer's personal link, already in their email. Opening it marks the document as viewed by the signer and adds that visit to the audit trail, so don't open it, share it, or show it in a chat. Keep envelope_id instead: the envelope is at /envelope/<envelope_id> in your dashboard.

Error Response

Response
{
  "success": false,
  "error": {
    "code": "INVALID_EMAIL",
    "message": "Valid recipient email is required"
  }
}

Verify Document

POST/api/verify

Verify 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 — read verificationType / hashComparison for 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 cases transaction_only collapses together apart: not_compared (no file sent, or no hash on record), match (always paired with full), and unrecognized (a file was compared and matched nothing on record). Branch on this rather than parsing message.

Request Example (JSON)

Request
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)

Request
curl -X POST https://www.easy-sign.ca/api/verify \
  -F "file=@signed-document.pdf" \
  -F "transactionId=ES-20241206-ABC12345"

Request Parameters

ParameterTypeDescription
transactionIdstringTransaction ID (format: ES-YYYYMMDD-XXXXXXXX). Found on signed documents.
fileFileOptional: Upload the signed PDF for full integrity verification

Success Response (Full Verification)

Response
{
  "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

StatusDescription
validDocument is fully signed. Read verificationType to know whether file integrity was also confirmed — full means it was, transaction_only means it was not.
pendingDocument exists but signing is not yet complete.
not_foundTransaction ID does not exist in our records.
voidedDocument 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.

Response
{
  "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.

Response
{
  "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": "Electronic signatures on documents signed via Easy Sign may have legal effect under Canadian provincial e-commerce legislation (e.g., Ontario Electronic Commerce Act, 2000) and, in federal contexts, PIPEDA Part 2, as well as ESIGN/UETA (US) and eIDAS (EU).",
    "auditTrail": "Complete audit trails are maintained for all signed documents."
  }
}

Integrations & Automation

Let an AI coding agent such as Claude Code or Codex send signature requests for you, or call the API from your own scripts.

Agent Skill for Claude Code and Codex

Our skills are open source on GitHub: synthmind-canada/Easy-Sign-Skills. The agent writes a plan, checks that each PDF belongs to the person it goes to, and sends only after you approve that plan. In Claude Code, install them with one command:

Claude Code
/plugin install easy-sign --marketplace synthmind-canada/Easy-Sign-Skills

Then ask in plain words, for example “send each client their engagement letter to sign”. The plugin gives the agent a short easy-sign command for checking, sending and verifying. In Codex, clone the repository and link the skills into ~/.agents/skills/. The README has the steps, including the two-step install for Claude Code versions older than 2.1.275.

Single-file version

If you'd rather not install anything, Claude Code and Codex both read skills written as a SKILL.md file. Save the file below in the folder your agent reads:

  • Claude Code: .claude/skills/easy-sign-send/SKILL.md in your project, or under ~/.claude/skills/ for every project
  • Codex: .agents/skills/easy-sign-send/SKILL.md in your project, or under ~/.agents/skills/ for every project
easy-sign-send/SKILL.md
---
name: easy-sign-send
description: Send a PDF for electronic signature with the Easy Sign API. Use when the user asks to send a PDF to someone for signature.
---

# Send a PDF for signature with Easy Sign

Every request emails a real person and cannot be recalled.
One request sends one PDF to one signer.

## Before you send
1. Confirm with the user: the signer's name and email, which PDF, the page and
   position of each field, and the envelope name. Send only after the user
   says yes in this conversation.
2. The API key is in the environment variable EASY_SIGN_API_KEY.
   Never print it or write it to a file.

## Build the request
- Local PDF (up to 3 MB): the example reads and base64-encodes it inside Node.
  Never put base64 on the command line: it quickly exceeds the shell's
  argument limit.
- PDF at a public URL (up to 20 MB): send "document": { "url": "https://..." }
  instead.
- Up to 15 pages. Pages are numbered from 1.
- Field types: signature, signature_date, date (filled with the date the
  request is sent), signed_datetime (filled in when the signer signs).
- Position each field with a preset (signature_bottom_right,
  signature_bottom_left, signature_bottom_center, signature_footer,
  date_after_signature) or with "position": { "x", "y", "width", "height" }
  in percent of the page, measured from its top-left corner.
- sender_timezone: the user's time zone. Easy Sign stamps the date and time
  of each signature in it. The example uses this computer's zone. A "date"
  field is different: it shows the date the request is sent, in UTC.
- Idempotency-Key: make a new one for every send the user approves (include
  the date and time, as in the example). Reuse it only to retry that same send.

Example: build the request and send it from Node. The output leaves out
signing_url.

```bash
node -e '
const fs = require("fs");
const path = require("path");
const [pdf, email, name, page, envelopeName, idempotencyKey] = process.argv.slice(1);
const body = {
  recipient: { email, name },
  document: {
    file_name: path.basename(pdf),
    base64: fs.readFileSync(pdf).toString("base64")
  },
  fields: [
    { type: "signature", page: Number(page), preset: "signature_bottom_right" },
    { type: "date", page: Number(page), preset: "date_after_signature" }
  ],
  envelope_name: envelopeName,
  sender_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
};
fetch("https://www.easy-sign.ca/api/agent/sign-request", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.EASY_SIGN_API_KEY,
    "Content-Type": "application/json",
    "Idempotency-Key": idempotencyKey
  },
  body: JSON.stringify(body)
}).then(async (res) => {
  const result = await res.json().catch(() => ({}));
  delete result.signing_url;
  console.log(JSON.stringify({
    http_status: res.status,
    retry_after: res.headers.get("retry-after"),
    replayed: res.headers.get("idempotent-replay") === "true",
    ...result
  }, null, 2));
});
' contract.pdf jane@example.com "Jane Smith" 1 "Contract Agreement" contract-agreement-jane-20261025-1403
```

## After you send
- replayed: true means this key was used before. Nothing new was sent; the
  result is the earlier send's. Tell the user.
- 201: tell the user the envelope_id, recipient_email and expires_at. The
  envelope is at https://www.easy-sign.ca/envelope/<envelope_id>.
- Never show or open signing_url. It is the signer's personal link, and
  opening it records a visit by the signer.
- The signing link stays valid for 7 days by default; Pro accounts can set up to 30 in Settings.
- 429 RATE_LIMITED: wait retry_after seconds, then retry with the same key.
- 409 IDEMPOTENCY_IN_PROGRESS: the first try is still running. Wait, then
  retry with the same key. Never switch to a new key here: that sends a
  second email.
- 5xx, or 401 DATABASE_ERROR: wait a few seconds, then retry with the same key.
- Any other 4xx: nothing was sent. Fix the request, confirm with the user
  again, then send with a new key.

Then ask in plain words, for example: “Send contract.pdf to Jane Smith (jane@example.com) for signature, bottom of page 2.” The agent confirms the details with you before anything is sent.

Network Access

Claude Code runs commands on your computer, so it can reach the API. Depending on your permission mode, it may ask you before it runs one. Codex runs commands in a sandbox with network access turned off by default. To let it call the API, allow network access in ~/.codex/config.toml:

~/.codex/config.toml
[sandbox_workspace_write]
network_access = true

MCP Tool Definition

We don't host an MCP server. If you wrap the API in your own, use this JSON schema for the tool:

MCP Tool Schema
{
  "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" },
          "locale": { "type": "string", "enum": ["en", "fr", "zh-hans"], "description": "Language of the invitation email and signing page (optional, defaults to en)" }
        },
        "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:

Terminal
# Add to your shell profile (~/.bashrc, ~/.zshrc, etc.)
export EASY_SIGN_API_KEY="es_live_your_api_key_here"

Start Claude Code or Codex from a terminal where the variable is set, so the agent can read it.

Example Workflow

Here's how a signing request might look from an agent that uses the skill. The request is sent from the account that owns the API key.

User

Send contract.pdf to John Lee (john@example.com) for signature. Signature and date at the bottom of page 2.

Assistant

Before I send: contract.pdf to John Lee (john@example.com), a signature and a date at the bottom right of page 2, envelope name “Contract Agreement”. Send it?

User

Yes.

Signature request sent.

John will get an email with a link to sign. The envelope ID is in the response, and the envelope is in your dashboard. The link stays valid for 7 days by default, up to 30 on Pro.

Best Practices

  • Confirm document and recipient details before sending
  • Store API keys in environment variables, never hardcode
  • Send an Idempotency-Key with every request, so a retry never emails the signer twice
  • Never open or share signing_url: it is the signer's personal link
  • Handle errors gracefully with clear feedback to users
  • Use meaningful envelope names for easy tracking

Error Codes

Sign Request API

CodeHTTPDescription
INVALID_FORMAT401Authorization value present but not a valid key (must start with es_live_)
INVALID_API_KEY401Authorization header missing or malformed, or key not recognized
KEY_REVOKED401API key has been revoked by the owner
KEY_EXPIRED401API key has expired
PRO_REQUIRED401API access requires an active Pro subscription
QUOTA_EXCEEDED403Envelope limit reached for your plan
DATABASE_ERROR401Temporary server error while validating the key — safe to retry
RATE_LIMITED429Too many requests (10/minute per key; contact us to raise)
INVALID_IDEMPOTENCY_KEY400Idempotency-Key exceeds 255 characters
IDEMPOTENCY_IN_PROGRESS409A request with the same Idempotency-Key is still processing
INVALID_JSON400Request body is not valid JSON
INVALID_EMAIL400Email format is invalid
SAME_EMAIL400Sender and recipient are the same
INVALID_LOCALE400recipient.locale is not one of en, fr, zh-hans
DOCUMENT_TOO_LARGE400Document exceeds the 20 MB (Pro plan) limit, or base64 exceeds 3 MB
DOCUMENT_TOO_MANY_PAGES400Document exceeds 15 page limit
INVALID_PDF400 / 500Invalid or corrupted PDF (400). Returned as 500 if server-side PDF tooling is temporarily unavailable — safe to retry.
INVALID_URL400Document URL is malformed or unsafe (SSRF protection)
DOCUMENT_URL_FAILED400Failed to download from URL
INVALID_FILE_NAME400File name exceeds 255 characters
NO_FIELDS400No signature fields provided
INVALID_FIELD_TYPE400Field type must be signature, signature_date, date, or signed_datetime
INVALID_FIELD_POSITION400Field position is invalid
STORAGE_EXCEEDED400Total storage limit reached for your plan
EMAIL_FAILED500Outbound signature email failed to send (request rolled back)
INTERNAL_ERROR500Unexpected 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.

StatusHTTPDescription
valid200Document signed. Integrity is confirmed only when verificationType is full; an uploaded file that matches no hash on record still returns valid + transaction_only.
pending200Document exists but signing is incomplete
voided200Sender voided the document — no longer legally valid
not_found400Transaction ID format invalid (must be ES-YYYYMMDD-XXXXXXXX)
not_found404Transaction ID does not exist or refers to a deleted record
not_found500Internal 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–30 days

Signing link validity (7 by default, up to 30 on Pro in Settings)

5

API keys per user

Need Help?

Have questions or need assistance? We're here to help.