eContract Developers

Quickstart

Send a PDF for signature, hand out the signing links and download the signed PDF with its audit trail

This walk-through sends a PDF to two signers with one API call, gets their signing links, and downloads the signed PDF and the audit trail once they have signed. Every request uses Authorization: Bearer cl_live_….

Prefer an AI client? Connect Claude, ChatGPT, Cursor or VS Code to the MCP server instead, no code needed.

1. Create an API key

  1. Sign up at econtract.online (free) and create a workspace.
  2. As the workspace owner or admin open Dashboard → API Keys → Create API Key.
  3. Tick contracts:read and contracts:write (add webhooks:write for step 7).
  4. Copy the key, it is shown only once, and keep it in an environment variable:
export ECONTRACT_API_KEY=cl_live_your_key_here

2. Send the PDF

POST /api/v1/contracts/send takes the document as base64 JSON, creates the contract and queues it. delivery: "link" means eContract sends no invitation emails: you will hand out the links yourself in step 4. Leave delivery out (or use "email") to let eContract email each signer instead.

# Build the JSON body (jq 1.6+; --rawfile avoids command-line length limits)
base64 < agreement.pdf | tr -d '\n' > agreement.b64
jq -n --rawfile b64 agreement.b64 '{
  title: "Service Agreement",
  signingOrderType: "parallel",
  signers: [
    {signerType: "external", name: "Alice Johnson", email: "[email protected]", signOrder: 1},
    {signerType: "external", name: "Bob Smith", email: "[email protected]", signOrder: 2}
  ],
  files: [{filename: "agreement.pdf", contentType: "application/pdf", contentBase64: $b64}],
  delivery: "link",
  expiresInDays: 30
}' > send.json

curl -X POST https://api.econtract.online/api/v1/contracts/send \
  -H "Authorization: Bearer $ECONTRACT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d @send.json

# keep the "id" of the response for the next steps
export CONTRACT_ID=8c1e2f3a-4b5c-4d6e-8f90-1a2b3c4d5e6f
import { readFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";

const API = "https://api.econtract.online/api/v1";
const headers = {
  Authorization: `Bearer ${process.env.ECONTRACT_API_KEY}`,
  "Content-Type": "application/json",
};

const pdf = await readFile("agreement.pdf");
const res = await fetch(`${API}/contracts/send`, {
  method: "POST",
  headers: { ...headers, "Idempotency-Key": randomUUID() },
  body: JSON.stringify({
    title: "Service Agreement",
    signingOrderType: "parallel",
    signers: [
      { signerType: "external", name: "Alice Johnson", email: "[email protected]", signOrder: 1 },
      { signerType: "external", name: "Bob Smith", email: "[email protected]", signOrder: 2 },
    ],
    files: [{ filename: "agreement.pdf", contentType: "application/pdf", contentBase64: pdf.toString("base64") }],
    delivery: "link",
    expiresInDays: 30,
  }),
});
if (res.status !== 202) throw new Error(JSON.stringify(await res.json()));
const contract = await res.json();
console.log(contract.id, contract.code, contract.processingStatus); // ... "processing"
import base64
import os
import uuid

import requests

API = "https://api.econtract.online/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['ECONTRACT_API_KEY']}"}

with open("agreement.pdf", "rb") as f:
    pdf_b64 = base64.b64encode(f.read()).decode()

res = requests.post(
    f"{API}/contracts/send",
    headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())},
    json={
        "title": "Service Agreement",
        "signingOrderType": "parallel",
        "signers": [
            {"signerType": "external", "name": "Alice Johnson", "email": "[email protected]", "signOrder": 1},
            {"signerType": "external", "name": "Bob Smith", "email": "[email protected]", "signOrder": 2},
        ],
        "files": [{"filename": "agreement.pdf", "contentType": "application/pdf", "contentBase64": pdf_b64}],
        "delivery": "link",
        "expiresInDays": 30,
    },
    timeout=120,
)
assert res.status_code == 202, res.json()
contract = res.json()
print(contract["id"], contract["code"], contract["processingStatus"])  # ... "processing"

Response: 202 Accepted

{
  "id": "8c1e2f3a-4b5c-4d6e-8f90-1a2b3c4d5e6f",
  "code": "CTR-20261010-K7Q2MX",
  "title": "Service Agreement",
  "status": "draft",
  "processingStatus": "processing",
  "signingOrderType": "parallel",
  "createdBy": "…",
  "createdAt": "2026-10-10T09:00:00.000Z",
  "updatedAt": "2026-10-10T09:00:00.000Z"
}

Other ways to give the document: files: [{ "url": "https://…/agreement.pdf" }], a template (templateId + templateValues), or a multipart upload. See AI agents for all options.

3. Wait until it is ready

Processing (storing the file, converting DOCX, adding signers, starting signing) takes a few seconds. Poll until processingStatus is ready:

curl https://api.econtract.online/api/v1/contracts/$CONTRACT_ID \
  -H "Authorization: Bearer $ECONTRACT_API_KEY"
# repeat until "processingStatus": "ready"  (status is then "in_signing")
async function waitUntilReady(id) {
  for (;;) {
    const c = await fetch(`${API}/contracts/${id}`, { headers }).then((r) => r.json());
    if (c.processingStatus === "ready") return c;
    if (c.processingStatus === "failed") throw new Error(c.processingError);
    await new Promise((r) => setTimeout(r, 3000));
  }
}
await waitUntilReady(contract.id);
import time

def wait_until_ready(contract_id):
    while True:
        c = requests.get(f"{API}/contracts/{contract_id}", headers=HEADERS).json()
        if c["processingStatus"] == "ready":
            return c
        if c["processingStatus"] == "failed":
            raise RuntimeError(c.get("processingError"))
        time.sleep(3)

wait_until_ready(contract["id"])
curl -X POST https://api.econtract.online/api/v1/contracts/$CONTRACT_ID/signing-links \
  -H "Authorization: Bearer $ECONTRACT_API_KEY"
const { data: links } = await fetch(`${API}/contracts/${contract.id}/signing-links`, {
  method: "POST",
  headers,
}).then((r) => r.json());

for (const link of links) {
  if (link.signingUrl) console.log(`${link.name} <${link.email}>: ${link.signingUrl}`);
}
links = requests.post(f"{API}/contracts/{contract['id']}/signing-links", headers=HEADERS).json()["data"]
for link in links:
    if link["signingUrl"]:
        print(f"{link['name']} <{link['email']}>: {link['signingUrl']}")
{
  "data": [
    { "signerId": "3a7f…", "name": "Alice Johnson", "email": "[email protected]", "signerType": "external",
      "status": "pending", "signingUrl": "https://econtract.online/sign/7Kf2…", "expiresAt": "2026-11-09T09:00:05.000Z" },
    { "signerId": "9b2c…", "name": "Bob Smith", "email": "[email protected]", "signerType": "external",
      "status": "pending", "signingUrl": "https://econtract.online/sign/Qm9x…", "expiresAt": "2026-11-09T09:00:05.000Z" }
  ]
}

Send each person their own link (chat, your app, your email). No email is sent by this call.

5. The signers sign

Each signer opens their link, confirms their identity with a code eContract emails to them, reviews the document and signs. Signing is always done by the person: API keys and OAuth tokens cannot sign (403 HUMAN_ACTION_REQUIRED).

Poll GET /api/v1/contracts/{id} (every few minutes is enough) until status is completed, or use the webhook from step 7. Each signer in signers shows pending, viewed, signed or rejected.

6. Download the signed PDF and the audit trail

curl -o signed.pdf -D - \
  "https://api.econtract.online/api/v1/contracts/$CONTRACT_ID/document?version=signed" \
  -H "Authorization: Bearer $ECONTRACT_API_KEY"
# headers include X-Document-Version: signed and X-Document-Sha256

curl https://api.econtract.online/api/v1/contracts/$CONTRACT_ID/audit-trail \
  -H "Authorization: Bearer $ECONTRACT_API_KEY"
import { writeFile } from "node:fs/promises";

const doc = await fetch(`${API}/contracts/${contract.id}/document?version=signed`, { headers });
if (!doc.ok) throw new Error((await doc.json()).code); // DOCUMENT_NOT_SIGNED until completed
await writeFile("signed.pdf", Buffer.from(await doc.arrayBuffer()));

const trail = await fetch(`${API}/contracts/${contract.id}/audit-trail`, { headers }).then((r) => r.json());
console.log(trail.chain.valid, trail.documentSha256 === doc.headers.get("X-Document-Sha256"));
doc = requests.get(f"{API}/contracts/{contract['id']}/document", params={"version": "signed"}, headers=HEADERS)
doc.raise_for_status()  # 409 DOCUMENT_NOT_SIGNED until completed
with open("signed.pdf", "wb") as f:
    f.write(doc.content)

trail = requests.get(f"{API}/contracts/{contract['id']}/audit-trail", headers=HEADERS).json()
print(trail["chain"]["valid"], trail["documentSha256"] == doc.headers["X-Document-Sha256"])

The audit trail lists the signers, every event (who, when, IP address, user agent), the result of the hash-chain check and the SHA-256 of the signed PDF.

7. Get notified with a webhook

Instead of polling, register an endpoint once (key with webhooks:write, created by a workspace owner or admin):

curl -X POST https://api.econtract.online/api/v1/webhooks \
  -H "Authorization: Bearer $ECONTRACT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example.com/webhooks/econtract",
    "events": ["contract.processing.completed", "contract.processing.failed", "signer.viewed", "signer.declined", "contract.completed"]
  }'

Store the secret from the response and verify X-Econtract-Signature on every delivery. On contract.completed, run step 6. Details: Webhooks.

Next steps

On this page