eDocYouSign API

Add end-to-end encrypted signatures, PowerForms and contract management to your own apps and to the tools your teams already use. REST, webhooks, an OpenAPI file and an SDK that handles the encryption.

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 URLhttps://YOUR-SITE/api/v1
FormatJSON (uploads use multipart/form-data)
AuthenticationBearer API key
SDKJavaScript SDK for Node 18+, Deno, Bun, browsers and serverless
Machine-readable spec/api/openapi.json

Quickstart

  1. 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.
  2. Check it works:
Shell
curl https://YOUR-SITE/api/v1/me \
  -H "Authorization: Bearer edys_xxxxxxxx_yourSecretKey"
  1. Send your first encrypted envelope with the SDK:
JavaScript (Node 18+)
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 it

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

AccessWhat it allows
Read onlyList and read envelopes, audit trails, documents (encrypted), PowerForms, agreements and webhooks
Read and writeEverything 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.

StepDetails
1. Document keyA random 256-bit key K
2. Encrypt the PDFAES-256-GCM with a random 12-byte IV. Upload "EDYS1\n" + IV + ciphertext + tag
3. Access code20 random characters from ABCDEFGHJKLMNPQRSTUVWXYZ23456789, shown as 5 groups of 4 (100 bits)
4. Wrap the keyPBKDF2-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. FingerprintplainHash = 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 codeGive 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

POST/api/v1/envelopeswrite

Send 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().

GET/api/v1/envelopes?status=sent&limit=50&offset=0read

List envelopes, newest first, with recipients and their status.

GET/api/v1/envelopes/{id}read

Status, recipients with their signing link (for embedding signing in your app), the wrapped key enc, the audit trail, and comment and markup counts.

GET/api/v1/envelopes/{id}/auditread

Every event with its time: created, emailed, viewed, signed, completed.

GET/api/v1/envelopes/{id}/documents/{original|signed}read

The encrypted file. SDK: eds.download(id, accessCode) returns the decrypted PDF.

POST/api/v1/envelopes/{id}/remindwrite

Body {"recipientId": "r1"}. Emails a recipient whose turn it is, at most once an hour.

POST/api/v1/envelopes/{id}/voidwrite

Body {"reason": "…"}. Signing links stop working and recipients are told.

GET/api/v1/envelopes/{id}/layoutread

Recipients 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

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

StatusMeaning
sentWaiting for signatures
signedEveryone signed; the sealed PDF is being produced
completedDone. The signed PDF and certificate are available
declinedA recipient declined
voidedThe sender cancelled it or sent a corrected version

Agreements (CLM)

GET/api/v1/agreements?status=activeread

The contract repository. status also includes expiring and expired, worked out from the dates.

GET/api/v1/agreements/{id}read

Including obligations, approvals and history.

POST/api/v1/agreementswrite

Create: title, type, counterparty, value, currency, effectiveDate, expiryDate, renewal (none, auto, manual), renewalMonths, noticeDays, tags, notes, stage, envelopeId.

PATCH/api/v1/agreements/{id}write

Change 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

GET/api/v1/powerformsread

Your 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:

POST/api/v1/webhookswrite

Body {"url": "https://…", "events": ["envelope.completed"]} (or ["*"] for all). The response includes the signing secret, shown once.

GET/api/v1/webhooksread

Your webhooks with their last response and failure count.

DELETE/api/v1/webhooks/{id}write

Stop sending to an endpoint.

EventWhen
envelope.sentAn envelope was sent
envelope.viewedA recipient opened their link
envelope.signedA recipient signed
envelope.completedEveryone signed and the PDF was sealed
envelope.declinedA recipient declined
envelope.voidedVoided or replaced by a corrected version
comment.addedA comment was posted (the text stays encrypted and isn't included)
powerform.submittedSomeone completed a PowerForm
agreement.createdAn agreement was created
agreement.updatedAn agreement changed

Payload

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

JavaScript
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… */ }
}
Python
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", ""))
PHP
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 id to 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.

JSON
{ "error": { "code": "insufficient_scope", "message": "This key is read-only…", "requestId": "req_8f2c…" } }
HTTPcodeWhat to do
400invalid_request, invalid_jsonFix the request; the message says what's wrong
401unauthorizedCheck the key, or create a new one
403insufficient_scope, plan_required, key_owner_inactiveUse a write key, upgrade, or recreate the key
404not_foundCheck the ID or path
409request_failedThe envelope's state doesn't allow this, for example it's already completed
413 / 415request_failedThe file is too large or isn't encrypted
422invalid_requestA field value isn't valid
429rate_limitedWait for Retry-After seconds. The default limit is 120 requests per minute per key
5xxrequest_failedRetry 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

Python 3
# 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 8
<?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.