AI agents
How an AI agent sends contracts for e-signature with eContract, end to end
An AI agent can do the whole contract job in eContract: prepare the document, send it, follow signing, give each signer their link, and collect the signed PDF and the audit trail. The one thing it can never do is sign.
Two ways in
| MCP server | REST API | |
|---|---|---|
| Best for | Chat assistants and IDE agents (Claude, ChatGPT, Cursor, VS Code) | Your own agent runtime, backends, workflows |
| Endpoint | https://api.econtract.online/mcp | https://api.econtract.online/api/v1/… |
| What you write | Nothing: the client lists the tools | HTTP calls (quickstart) |
| Auth | OAuth (the client handles it) or an API key header | API key or OAuth access token |
| Documents | Up to 10 MB through the connector | Up to 50 MB per file; a request body is limited to 50 MB, so use files[].url for large files (limits) |
| Events | Poll get_contract | Poll or webhooks |
Both paths reach the same API with the same rules: scopes, role cap, rate limits, idempotency and the human-signs rule.
Signing is human
API keys and OAuth tokens are never allowed to sign or decline on anyone's behalf, manage saved signatures or signing certificates. Those calls answer 403 HUMAN_ACTION_REQUIRED. Instead:
- External signers get a personal link (
https://econtract.online/sign/<token>). They open it, confirm their identity with a one-time code sent to their email, and sign or decline themselves. - Internal signers (members of the workspace) get a link to the contract in the dashboard and sign there while signed in.
- With sequential signing each signer can sign only after the previous ones.
Rules for agent builders:
- Confirm with the user before sending, voiding or reminding: these contact real people.
- Give each signer only their own link, privately. A link lets its holder act as that signer once they pass the email check.
- Never pretend a contract is signed until
statusiscompleted.
Choose a credential
| API key | OAuth | |
|---|---|---|
| Acts as | The admin who created the key | The person who connected the app |
| Set up | Dashboard → API Keys, paste the key into your config | The user clicks Connect and approves in the browser |
| Workspace | The key's workspace | The workspace the user picked on the consent screen |
| Scopes | Ticked when the key is created | Requested by the app, can be reduced by the user |
| Lifetime | Until revoked or its expiry date | Access token 1 h, refresh token 30 days (rotating), grant until revoked |
| Revoke | Dashboard → API Keys | The user: Dashboard → Connected apps |
| Pick it when | One team runs the agent for its own workspace, headless | Many users connect their own workspaces, or you ship a client app |
Minimal scopes for the flow below: contracts:read contracts:write. Add templates:read for templates, webhooks:write to register webhooks, ai:use for AI analysis.
The flow
send ──► processing ──► ready (in_signing) ──► signing links ──► people sign ──► completed
│ │ │
poll GET /contracts/{id} POST /signing-links signed PDF + audit trail
or webhook contract.processing.* (or email invitations) GET /document, /audit-trail1. Send
POST /api/v1/contracts/send creates the contract, stores the document and queues it for processing, all in one call. Give exactly one document source:
| Source | Field |
|---|---|
| Inline file | files: [{ "filename": "nda.pdf", "contentBase64": "JVBERi0…" }] (PDF or DOCX, optional contentType) |
| File on the web | files: [{ "url": "https://example.com/nda.pdf" }] (https, public host, no redirects, 30 s, 50 MB) |
| Template | templateId + templateValues |
| Uploaded earlier | fileKey (from POST /api/v1/files/upload) |
| Multipart | multipart/form-data with files parts and a metadata JSON string |
Up to 10 files; DOCX files are converted to PDF. Choose how signers hear about it with delivery:
delivery | Effect |
|---|---|
email (default) | eContract emails each signer an invitation (in order for sequential signing) |
link | No invitation emails at all, also not for the following signers in sequential order: fetch the links and hand each one out yourself |
both | Invitations are emailed and you can fetch links too |
{
"title": "Mutual NDA - Acme / Globex",
"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": "nda.pdf", "contentBase64": "JVBERi0xLjcK…" }],
"delivery": "link",
"expiresInDays": 30
}The answer is 202 Accepted with the contract in processingStatus: "processing". Send an Idempotency-Key so a retry cannot send the contract twice.
2. Wait for processing
Poll GET /api/v1/contracts/{id} every 2–5 seconds until processingStatus is ready (the contract is then in_signing), or subscribe to contract.processing.completed and contract.processing.failed. On failure processingError says why and the contract stays visible with processingStatus: "failed"; fix the input and send again.
3. Get signing links
POST /api/v1/contracts/{id}/signing-links (contracts:write) returns a link for every signer. It sends no email.
{
"data": [
{
"signerId": "3a7f…",
"name": "Alice Johnson",
"email": "[email protected]",
"signerType": "external",
"status": "pending",
"signingUrl": "https://econtract.online/sign/7Kf2…",
"expiresAt": "2026-11-09T09:00:00.000Z"
},
{
"signerId": "9b2c…",
"name": "Bob Smith",
"email": "[email protected]",
"signerType": "external",
"status": "signed",
"signingUrl": null,
"expiresAt": null
}
]
}- Every call mints a fresh link for each signer who still has to act; older links keep working.
- Signers who already signed or declined get
signingUrl: null. - Internal signers get the dashboard link to the contract and
expiresAt: null. - External links expire with the contract (30 days when the contract has no later expiry).
- Only while the contract is
pendingorin_signing; otherwise409 CONTRACT_NOT_IN_SIGNING(also while it is still processing).
To email an invitation later instead, call POST /api/v1/contracts/{id}/resend-signing-request (optionally { "signerId": "…" }).
4. Follow signing
GET /api/v1/contracts/{id} lists each signer with status (pending, notified, viewed, signed, rejected, expired) and timestamps. Or listen for signer.viewed, signer.completed (one per signature), signer.declined and contract.completed. A decline voids the contract. To cancel it yourself: POST /api/v1/contracts/{id}/void with an optional reason.
5. Collect the signed PDF and the audit trail
When status is completed:
curl -o signed.pdf -D headers.txt \
"https://api.econtract.online/api/v1/contracts/{id}/document?version=signed" \
-H "Authorization: Bearer cl_live_your_key_here"| Query | Returns |
|---|---|
| (none) | The signed PDF when completed, otherwise the current file |
version=signed | The signed PDF; 409 DOCUMENT_NOT_SIGNED until completed |
version=original | The unsigned upload (the PDF rendition of a DOCX); 404 ORIGINAL_NOT_AVAILABLE if it was not kept |
fileId=… | A specific file of a multi-file contract (default: the first) |
Response headers: Content-Type: application/pdf, Content-Disposition: attachment; filename="…-signed.pdf", X-Document-Version (signed, original or current) and X-Document-Sha256.
GET /api/v1/contracts/{id}/audit-trail returns the evidence in JSON:
{
"contract": { "id": "8c1e…", "code": "CTR-…", "title": "Mutual NDA - Acme / Globex", "status": "completed", "createdAt": "…", "completedAt": "…" },
"signers": [
{ "id": "3a7f…", "name": "Alice Johnson", "email": "[email protected]", "signerType": "external", "status": "signed",
"signOrder": 1, "notifiedAt": null, "viewedAt": "…", "signedAt": "…", "rejectedAt": null, "rejectionReason": null, "ipAddress": "…" }
],
"events": [
{ "type": "created", "source": "activity", "actorName": "…", "actorEmail": "…", "at": "…", "ip": "…", "userAgent": "…", "details": {} },
{ "type": "signer_viewed", "source": "workflow", "actorName": "Alice Johnson", "actorEmail": "[email protected]", "at": "…", "ip": null, "userAgent": null, "details": { "signerId": "3a7f…" } }
],
"chain": { "valid": true, "checkedAt": "…", "totalEvents": 7, "chainedEvents": 7, "verifiedEvents": 7, "unchainedEvents": 0 },
"documentSha256": "4f1c…",
"documentVersion": "signed"
}eventsmerges the hash-chained contract activity log (source: "activity") with signing workflow events (source: "workflow"), oldest first.chain.validsays whether the activity hash chain verifies (brokenAtpoints to the first broken link otherwise).documentSha256is the hash of the fileGET /documentreturns by default; compare it withX-Document-Sha256of your download.
With MCP
The same flow with tools: send_contract → get_contract (until ready) → get_signing_links → get_contract / send_reminder → download_signed_document + get_audit_trail. Connect a client in a minute: MCP server.
Checklist
- Credential with the smallest scope set (
contracts:read contracts:writeto start) - User confirmation before
send,voidand reminders -
Idempotency-Keyon every write, the same key on retries - Links handed to each signer privately, never signed by the agent
- Webhook signature verification and de-duplication by event
id - Backoff on
429(Retry-After) and on5xx