Overview
The eDocYouSign API lets your applications send documents for signature, track their progress, download signed copies, manage agreements and receive real-time events. It's a JSON REST API with an OpenAPI 3 description, so you can import it into Postman, Microsoft Power Automate, Zapier, code generators and API gateways.
- Available on Business and Enterprise plans. Workspace owners create keys under API & webhooks.
- End-to-end encrypted. Documents are encrypted in your code before they're uploaded. The API never sees a readable document, and neither does anyone with access to the server.
- Same rules as the app. API calls run as the key's creator, with the same permissions, sending limits, emails and audit trail.
| Base URL | https://YOUR-SITE/api/v1 |
| Format | JSON (uploads use multipart/form-data) |
| Authentication | Bearer API key |
| SDK | JavaScript SDK for Node 18+, Deno, Bun, browsers and serverless |
| Machine-readable spec | /api/openapi.json |
Quickstart
- Sign in as a workspace owner on a Business or Enterprise plan, open API & webhooks and create a key with Allow changes ticked. Copy it; it's shown once.
- Check it works:
curl https://YOUR-SITE/api/v1/me \
-H "Authorization: Bearer edys_xxxxxxxx_yourSecretKey"- Send your first encrypted envelope with the SDK:
import fs from "node:fs";
import { EDocYouSign } from "./edocyousign.js"; // download from https://YOUR-SITE/api/sdk/edocyousign.js
const eds = new EDocYouSign({ baseUrl: "https://YOUR-SITE", apiKey: process.env.EDOCYOUSIGN_KEY });
const { id, accessCode } = await eds.send({
pdf: fs.readFileSync("contract.pdf"),
title: "Consulting agreement",
message: "Please review and sign.",
recipients: [{ name: "Sam Lee", email: "sam@example.com" }],
fields: [
{ recipient: 0, type: "signature", page: 0, x: 72, y: 600, w: 160, h: 40 },
{ recipient: 0, type: "date", page: 0, x: 260, y: 620, w: 100, h: 18 },
],
});
console.log(id, accessCode); // text the access code to Sam; the email never contains itSam gets a signing email. When everyone has signed, you get an envelope.completed webhook and can download the signed PDF with eds.download(id, accessCode).
Authentication and permissions
Send the key in the Authorization header on every request: Authorization: Bearer edys_xxxxxxxx_…. Keys are stored only as a one-way hash, so a lost key can't be recovered; revoke it and create a new one.
| Access | What it allows |
|---|---|
| Read only | List and read envelopes, audit trails, documents (encrypted), PowerForms, agreements and webhooks |
| Read and write | Everything above, plus send, void and remind envelopes, create and update agreements, and manage webhooks |
- A key acts as the person who created it. Owners and admins see every envelope in the workspace; members see their own.
- Keys stop working when they're revoked, when their creator leaves the workspace or is disabled, or when the workspace leaves the Business plan.
- Create one key per integration, so you can revoke one without breaking the others. Keep keys in server-side secret settings, never in a web page or mobile app.
End-to-end encryption
Every document is encrypted by the sender before upload. The SDK does this for you; if you write your own client, follow these steps exactly.
| Step | Details |
|---|---|
| 1. Document key | A random 256-bit key K |
| 2. Encrypt the PDF | AES-256-GCM with a random 12-byte IV. Upload "EDYS1\n" + IV + ciphertext + tag |
| 3. Access code | 20 random characters from ABCDEFGHJKLMNPQRSTUVWXYZ23456789, shown as 5 groups of 4 (100 bits) |
| 4. Wrap the key | PBKDF2-HMAC-SHA256 with 310,000 iterations and a random 16-byte salt over the code without dashes gives a 256-bit key-encryption key. AES-256-GCM-encrypt K with it: wrapped = IV + ciphertext + tag (60 bytes) |
| 5. Fingerprint | plainHash = SHA-256 of the unencrypted PDF, in hex |
| 6. Recovery (optional) | If GET /me returns recoveryKey, also send escrow = RSA-OAEP-SHA-256 encryption of K with it, so two-person recovery works |
| 7. Share the code | Give the access code to signers by phone, SMS or your own app. Never put it in the same email as the signing link. |
The server never receives K or the access code, so it can't read the document. Signers enter the code in their browser. To read a signed copy, download it and unwrap K with the same code.
Envelopes
/api/v1/envelopeswriteSend an encrypted document. multipart/form-data with meta (JSON) and pdf (encrypted file). Returns id, agreementId and any failed emails. Use the SDK's send().
/api/v1/envelopes?status=sent&limit=50&offset=0readList envelopes, newest first, with recipients and their status.
/api/v1/envelopes/{id}readStatus, recipients with their signing link (for embedding signing in your app), the wrapped key enc, the audit trail, and comment and markup counts.
/api/v1/envelopes/{id}/auditreadEvery event with its time: created, emailed, viewed, signed, completed.
/api/v1/envelopes/{id}/documents/{original|signed}readThe encrypted file. SDK: eds.download(id, accessCode) returns the decrypted PDF.
/api/v1/envelopes/{id}/remindwriteBody {"recipientId": "r1"}. Emails a recipient whose turn it is, at most once an hour.
/api/v1/envelopes/{id}/voidwriteBody {"reason": "…"}. Signing links stop working and recipients are told.
/api/v1/envelopes/{id}/layoutreadRecipients and field positions (never values), to build a corrected copy. Send it with replaces: "{old id}" in meta to void the old one automatically.
The meta object
{
"title": "Consulting agreement",
"subject": "Please sign: Consulting agreement",
"message": "Hi Sam, please review and sign.",
"ordered": false,
"pages": 3,
"enc": { "v": 1, "salt": "…", "iter": 310000, "wrapped": "…", "plainHash": "…", "escrow": "…" },
"recipients": [ { "id": "r1", "name": "Sam Lee", "email": "sam@example.com", "color": "#2D78D6" } ],
"fields": [ { "type": "signature", "recipientId": "r1", "page": 0, "x": 72, "y": 600, "w": 160, "h": 40, "required": true } ],
"agreement": { "type": "Master services agreement", "counterparty": "Acme", "value": 25000, "expiry_date": "2027-12-31", "renewal": "auto" }
}Fields: signature, initials, name, date, email, text, checkbox and attachment. Positions are in PDF points measured from the top-left of the page, and page starts at 0. A US Letter page is 612 × 792 points. Add agreement to track the envelope as an agreement (Business plan).
Statuses
| Status | Meaning |
|---|---|
| sent | Waiting for signatures |
| signed | Everyone signed; the sealed PDF is being produced |
| completed | Done. The signed PDF and certificate are available |
| declined | A recipient declined |
| voided | The sender cancelled it or sent a corrected version |
Agreements (CLM)
/api/v1/agreements?status=activereadThe contract repository. status also includes expiring and expired, worked out from the dates.
/api/v1/agreements/{id}readIncluding obligations, approvals and history.
/api/v1/agreementswriteCreate: title, type, counterparty, value, currency, effectiveDate, expiryDate, renewal (none, auto, manual), renewalMonths, noticeDays, tags, notes, stage, envelopeId.
/api/v1/agreements/{id}writeChange any of those fields, or move stage (draft, review, approval, sent, signed, active, renewed, terminated).
Typical use: sync agreements to your CRM opportunity, ERP vendor record or contract register, and drive renewal tasks from agreement.updated events.
PowerForms
/api/v1/powerformsreadYour reusable signing links with response counts. Each response arrives as a completed envelope and fires powerform.submitted.
PowerForms are created in the envelope editor because the form's key lives only in its link. Embed the link on your website, or print its QR code.
Webhooks
Webhooks push events to your HTTPS endpoint as they happen, so you don't need to poll. Add them on the API & webhooks page or with the API:
/api/v1/webhookswriteBody {"url": "https://…", "events": ["envelope.completed"]} (or ["*"] for all). The response includes the signing secret, shown once.
/api/v1/webhooksreadYour webhooks with their last response and failure count.
/api/v1/webhooks/{id}writeStop sending to an endpoint.
| Event | When |
|---|---|
envelope.sent | An envelope was sent |
envelope.viewed | A recipient opened their link |
envelope.signed | A recipient signed |
envelope.completed | Everyone signed and the PDF was sealed |
envelope.declined | A recipient declined |
envelope.voided | Voided or replaced by a corrected version |
comment.added | A comment was posted (the text stays encrypted and isn't included) |
powerform.submitted | Someone completed a PowerForm |
agreement.created | An agreement was created |
agreement.updated | An agreement changed |
Payload
{
"id": "evt_5c1f0a9e3b7d42a1c8e6f0b2",
"event": "envelope.completed",
"created": "2026-10-17T14:02:11+00:00",
"workspace": { "id": 12, "name": "Acme Legal" },
"data": {
"id": "a0e72ebae6374980", "title": "Consulting agreement", "status": "completed",
"sender": { "name": "Ann Owner", "email": "ann@acme.co" },
"recipients": [ { "id": "r1", "name": "Sam Lee", "email": "sam@example.com", "status": "signed", "signedAt": "2026-10-17T10:02:09-04:00" } ]
}
}Verifying webhooks
Every request has X-eDocYouSign-Signature: t=<unix time>,v1=<hex>, where v1 is HMAC-SHA256 of t + "." + raw body with your webhook secret. Check it, and reject anything older than 5 minutes.
import { EDocYouSign } from "./edocyousign.js";
// Express: app.post("/hooks/edocyousign", express.text({ type: "*/*" }), handler)
async function handler(req, res) {
const ok = await EDocYouSign.verifyWebhook(req.body, req.get("X-eDocYouSign-Signature"), process.env.EDOCYOUSIGN_WEBHOOK_SECRET);
if (!ok) return res.sendStatus(400);
const evt = JSON.parse(req.body);
res.sendStatus(200); // answer within 8 seconds, then do the work
if (evt.event === "envelope.completed") { /* update your CRM, file the record… */ }
}import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", 0))
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", ""))function verify_edocyousign(string $raw, string $header, string $secret): bool {
parse_str(str_replace(',', '&', $header), $p);
if (abs(time() - (int) ($p['t'] ?? 0)) > 300) return false;
return hash_equals(hash_hmac('sha256', $p['t'] . '.' . $raw, $secret), (string) ($p['v1'] ?? ''));
}- Retries: non-2xx answers or timeouts are retried after 1 minute, 5 minutes, 30 minutes, then 2, 6 and 24 hours. Delivery history is on the API page.
- Duplicates: use the event
idto ignore repeats. - Order: events usually arrive in order, but read
GET /envelopes/{id}when order matters. - Data: events carry names, emails and statuses, never document content, field values, signing links or keys.
Errors and limits
Errors return a JSON body with a stable code, a readable message and a requestId to quote to support.
{ "error": { "code": "insufficient_scope", "message": "This key is read-only…", "requestId": "req_8f2c…" } }| HTTP | code | What to do |
|---|---|---|
| 400 | invalid_request, invalid_json | Fix the request; the message says what's wrong |
| 401 | unauthorized | Check the key, or create a new one |
| 403 | insufficient_scope, plan_required, key_owner_inactive | Use a write key, upgrade, or recreate the key |
| 404 | not_found | Check the ID or path |
| 409 | request_failed | The envelope's state doesn't allow this, for example it's already completed |
| 413 / 415 | request_failed | The file is too large or isn't encrypted |
| 422 | invalid_request | A field value isn't valid |
| 429 | rate_limited | Wait for Retry-After seconds. The default limit is 120 requests per minute per key |
| 5xx | request_failed | Retry with backoff |
The sending limit of the app also applies (200 envelopes per sender per day by default; the site operator can raise it).
Connect your applications
Use these recipes to connect eDocYouSign to the tools your teams already use. They all rest on the same three things: the REST API, webhooks and the OpenAPI file.
Zapier
Trigger: Webhooks by Zapier → Catch Hook, then paste its URL as a webhook (events such as envelope.completed). Action: Webhooks by Zapier → Custom Request with your Bearer key to call the API, or a Code by Zapier step with the SDK.
Make (Integromat)
Add a Custom webhook module and register its address as a webhook. Use the HTTP → Make a request module with the Authorization header for API calls.
Microsoft Power Automate
Data → Custom connectors → Import an OpenAPI file and choose /api/openapi.json, with API key authentication (header Authorization, value Bearer …). Use When an HTTP request is received as the webhook trigger. Works with Teams, SharePoint, Outlook and Dynamics 365.
Salesforce
Store the key in a Named Credential. Call POST /envelopes from Apex (encrypt with the SDK in a small serverless function, or the Python or PHP example). Point a webhook at a Salesforce Site or Apex REST endpoint to update the Opportunity when envelope.completed arrives.
HubSpot
Workflows: a Send a webhook action starts an envelope through your function; a custom code action with the key reads status. Register a webhook to log envelope.signed as a timeline event.
Slack and Microsoft Teams
Send envelope.completed and agreement.updated events through Zapier, Make or Power Automate to a channel, or forward them from a small function to an incoming-webhook URL. Events never include document content.
Google Workspace
Apps Script UrlFetchApp.fetch with the Authorization header reads envelopes and agreements into Sheets. Deploy a web app as a webhook endpoint to update rows in real time.
n8n, Workato, Tray, Boomi, MuleSoft
Import the OpenAPI file or use an HTTP node with Bearer auth, plus a webhook trigger node. The API follows standard REST conventions.
HR, ERP and ITSM (Workday, SAP, NetSuite, ServiceNow)
Use their REST or outbound-message features: create an agreement or envelope on an event, and close tasks when envelope.completed arrives.
Postman and Insomnia
Import /api/openapi.json to get every request ready to run. Set a bearerToken variable to your key.
Your own website or portal
Create the envelope on your server, then show the recipient their signing link from GET /envelopes/{id} inside your app (embedded signing), with the access code shown on your own authenticated page.
Serverless (AWS Lambda, Azure Functions, Cloudflare Workers, Vercel)
The SDK runs anywhere with Web Crypto, so you can encrypt and send from a function, and verify webhooks with EDocYouSign.verifyWebhook.
SDK and code examples
JavaScript SDK
Download edocyousign.js. One file, no dependencies. Methods: me(), send(), download(), envelopes.list/get/audit/remind/void, agreements.list/get/create/update, webhooks.list/create/delete, powerforms.list, and EDocYouSign.verifyWebhook(). Errors throw EDocYouSignError with status, code and requestId. GET requests retry automatically on 429 and 5xx.
Python
# pip install requests cryptography
import os, json, base64, hashlib, secrets, requests
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
BASE = os.environ.get("EDOCYOUSIGN_URL", "https://edocyousign.com") + "/api/v1"
KEY = os.environ["EDOCYOUSIGN_KEY"]
AUTH = {"Authorization": f"Bearer {KEY}"}
ALPHA = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
def new_access_code():
raw = secrets.token_bytes(20)
c = "".join(ALPHA[b & 31] for b in raw)
return "-".join(c[i:i + 4] for i in range(0, 20, 4))
def seal(key, data):
iv = secrets.token_bytes(12)
return iv + AESGCM(key).encrypt(iv, data, None)
def wrap_for_code(doc_key, code):
salt = secrets.token_bytes(16)
kek = PBKDF2HMAC(hashes.SHA256(), 32, salt, 310000).derive(code.replace("-", "").upper().encode())
return {"v": 1, "salt": base64.b64encode(salt).decode(), "iter": 310000, "wrapped": base64.b64encode(seal(kek, doc_key)).decode()}
def send(pdf_bytes, title, recipients, fields):
doc_key, code = secrets.token_bytes(32), new_access_code()
enc = wrap_for_code(doc_key, code)
enc["plainHash"] = hashlib.sha256(pdf_bytes).hexdigest()
meta = {"title": title, "pages": 1, "enc": enc,
"recipients": [{"id": f"r{i + 1}", "name": r["name"], "email": r["email"], "color": "#2D78D6"} for i, r in enumerate(recipients)],
"fields": fields}
files = {"meta": (None, json.dumps(meta)), "pdf": ("document.enc", b"EDYS1\n" + seal(doc_key, pdf_bytes), "application/octet-stream")}
r = requests.post(f"{BASE}/envelopes", headers=AUTH, files=files, timeout=60)
r.raise_for_status()
return r.json()["id"], code # keep the access code safe; share it with signers by phone or SMS
if __name__ == "__main__":
envelope_id, access_code = send(open("contract.pdf", "rb").read(), "Consulting agreement",
[{"name": "Sam Lee", "email": "sam@example.com"}],
[{"type": "signature", "recipientId": "r1", "page": 0, "x": 72, "y": 600, "w": 160, "h": 40}])
print(envelope_id, access_code)PHP
<?php
// PHP 8+: encrypt a PDF and send it with the eDocYouSign API.
$base = getenv('EDOCYOUSIGN_URL') . '/api/v1';
$key = getenv('EDOCYOUSIGN_KEY');
function seal(string $key, string $data): string {
$iv = random_bytes(12);
$ct = openssl_encrypt($data, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
return $iv . $ct . $tag; // same layout as Web Crypto: IV, ciphertext, 16-byte tag
}
function new_access_code(): string {
$a = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; $c = '';
foreach (str_split(random_bytes(20)) as $b) $c .= $a[ord($b) & 31];
return implode('-', str_split($c, 4));
}
$pdf = file_get_contents('contract.pdf');
$docKey = random_bytes(32);
$code = new_access_code();
$salt = random_bytes(16);
$kek = hash_pbkdf2('sha256', str_replace('-', '', $code), $salt, 310000, 32, true);
$meta = [
'title' => 'Consulting agreement', 'pages' => 1,
'enc' => ['v' => 1, 'salt' => base64_encode($salt), 'iter' => 310000, 'wrapped' => base64_encode(seal($kek, $docKey)), 'plainHash' => hash('sha256', $pdf)],
'recipients' => [['id' => 'r1', 'name' => 'Sam Lee', 'email' => 'sam@example.com', 'color' => '#2D78D6']],
'fields' => [['type' => 'signature', 'recipientId' => 'r1', 'page' => 0, 'x' => 72, 'y' => 600, 'w' => 160, 'h' => 40]],
];
$tmp = tempnam(sys_get_temp_dir(), 'eds');
file_put_contents($tmp, "EDYS1\n" . seal($docKey, $pdf));
$ch = curl_init("$base/envelopes");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $key"],
CURLOPT_POSTFIELDS => ['meta' => json_encode($meta), 'pdf' => new CURLFile($tmp, 'application/octet-stream', 'document.enc')]]);
$res = json_decode(curl_exec($ch), true);
unlink($tmp);
echo ($res['id'] ?? json_encode($res)) . " access code: $code\n";Security and versioning
- HTTPS only in production. Keys are hashed with SHA-256; webhook secrets are encrypted at rest with AES-256-GCM.
- Webhooks only go to public HTTPS addresses (no private or local networks), and never follow redirects.
- Every API action is written to the workspace audit log and, for envelopes, to the envelope's audit trail.
- The API is versioned in the path (
/v1). New fields and events may be added at any time, so ignore fields you don't recognize. Breaking changes come as a new version, announced in advance.