# eSignStack API integration guide > How to integrate with the eSignStack electronic signature API at /api/v1: keys, requests, envelopes, signing sessions, webhooks, limits and MCP. The endpoint catalog is the OpenAPI file at /docs/api.json; use only endpoints listed there. Every docs page is also available as Markdown at /docs/{slug}.md, listed in /llms.txt. ## Requests Base URL: https://esignstack.app/api/v1. JSON keys are snake_case. Objects use prefixed IDs (env_, rcp_, doc_, fld_, tpl_, file_, fdr_, frl_, ws_, evt_), never internal numbers. Every object has object, id, created (UTC timestamp) and livemode. Ignore response fields you don't recognize; new ones can appear at any time. ## Keys Send Authorization: Bearer . Browser sessions can't call the API. - rk_live_ / rk_test_: workspace keys, bound to one workspace. Created in the app under Developers > API keys by workspace owners, admins and developers (developers can only grant webhooks:read, webhooks:write and keys:write). - sk_live_ / sk_test_: platform secret keys, created in the platform console. Send Workspace-Id: ws_... to act in a workspace the platform owns; without it a platform key can only call platform-level endpoints. A workspace key with a different Workspace-Id is rejected with 403. Secrets are shown once. Each key has explicit abilities, no wildcards: envelopes:read, envelopes:write, envelopes:send, envelopes:void, templates:read, templates:write, folders:read, folders:write, files:write, artifacts:read, webhooks:read, webhooks:write, brand_kits:read, brand_kits:write, usage:read, workspaces:export, keys:write, audit:read (opt-in: the workspace activity log). Platform keys can also hold platform:read (GET /platform and GET /platform/members, read-only: settings and staff change only in the console), workspaces:read, workspaces:write, login_links:write and identity_providers:write. A key created through POST /api_keys has its creator's mode (a test key only issues test keys) and can't have abilities its creator lacks or outlive it. Revoke with DELETE /api_keys/{key}. GET /api_keys/current returns the calling key's details. Test keys see only test (sandbox) data and live keys only live data. Test envelopes don't count toward limits, every page is marked as a test, emails go only to verified members of the workspace, and sending with SMS verification returns 422. Brand kit, folder and filing rule writes and identity providers need a live key (403 live_key_required); folders and filing rules are shared by both modes. ## Versions The current version is 2026-10-14. Keys are pinned to the version current when created. The Api-Version header overrides it per request; unsupported values return 400 api_version_unsupported. Responses include Api-Version. ## Idempotency Send Idempotency-Key (1 to 255 characters) on POST requests and reuse it when retrying the same request. Method, path, query, body, Workspace-Id and Api-Version must match; JSON key order doesn't matter, array order does. Completed responses are kept for 24 hours and replayed with Idempotent-Replayed: true. A different request with the same key returns 409 idempotency_key_reused. A request still running returns 409 idempotency_key_in_progress with Retry-After. Failed requests roll back and don't use up the key. The header is ignored on POST /verify, POST /login_identities/{id}/deprovision and POST /identity_providers/{idp}/subjects/{sub}/deprovision. ## Lists limit (1 to 100, default 20), starting_after or ending_before (not both). Lists return {object: "list", data, has_more, url}, newest first. Pass the last ID as starting_after for the next page. Envelope reads accept expand[]=recipients, fields and documents. List filters use filter[...]: GET /envelopes filter[status], filter[external_id], filter[template], filter[created][gte|lte], filter[metadata][key], filter[archived]=true; GET /templates filter[created], filter[metadata], filter[archived]; GET /folders filter[type], filter[parent]; GET /events filter[type], filter[object], filter[created][gt|gte|lt|lte]; deliveries filter[status]; GET /workspaces filter[external_id|status|name]; GET /audit_events filter[event]; GET /platform/members filter[role]. An unknown filter or value returns 422. ## Errors { "error": { "type", "code", "message", "param", "doc_url", "request_id" } } Types: invalid_request_error, authentication_error, permission_error, rate_limit_error, idempotency_error, api_error. Common codes: invalid_request, workspace_required, api_version_unsupported (400); authentication_required (401); permission_denied, live_key_required (403); resource_missing (404); resource_conflict, correction_requires_void, idempotency_key_reused, idempotency_key_in_progress (409); validation_failed (422); rate_limit_exceeded (429). Every response has a Request-Id header; keep it for support and never log keys or signing URLs. ## Rate limits 100 requests a second per key and 6,000 a minute per workspace across all keys. Responses carry RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and RateLimit-Policy. Over the limit: 429 with Retry-After. ## API envelope limits An envelope counts when it is sent through the API or MCP in live mode, or when the API issues a signing session for an envelope sent another way. Test envelopes never count. - Free: part of the plan's 5 envelopes a month (app and API together). - Starter: 50 a month (600 a year up front on yearly plans). - Pro: 200 a month (2,400 a year). - Platform: 500 a month included (6,000 a year), then 25 cents each, with an optional monthly spending cap set on the Billing page. A refused send returns 422 validation_failed with param "document" and error.quota.code: envelope_limit_reached, api_envelope_limit_reached, api_spending_cap_reached or api_overage_unavailable, plus error.quota.resets_at. GET /usage (usage:read) returns api_envelopes with used, limit, remaining and resets_at; a platform key without Workspace-Id also gets platform.api_envelopes, the platform's own allowance, extras (overage) and spending_cap. ## Envelopes Status: draft, sent, completed, voided, declined, expired. 1. POST /files with a multipart file (PDF or DOCX) or a public HTTPS url to a PDF. Optional password, use_text_tags (default true) and import_form_fields (false uses a fillable PDF as a plain document). Wait for status ready (GET /files/{file} or the file.ready event). 2. POST /envelopes with file and name (optional external_id, metadata, email_subject, email_message, expires_in_days, send_emails, notification_policy, folder). A file that isn't ready returns 409. 3. PUT /envelopes/{id}/documents to assemble documents from pages of files or existing documents. visible_to is a list of rcp_ IDs: null means everyone, [] means no one. 4. POST /envelopes/{id}/recipients. type: signer, in_person_signer, approver, viewer (shown as "Needs to acknowledge"), cc (shown as "Gets a copy"). An in_person_signer signs on their host's device in the eSignStack app, never from a link: host_email names the host (a workspace member; defaults to the envelope's sender), email is optional (only for their copy), they get no signing or reminder emails and can't have a signing session through the API. routing_order 1 to 99. auth_method none, access_code, sms or email_otp; anything but none needs the Platform plan. allow_delegation defaults to false. embedded: true (DocuSign clientUserId) marks a recipient who signs only in your app through signing sessions: eSignStack sends them no invitation, reminder, next-signer or "document updated" email, while recipient.activated and other webhooks still fire and their completed copy, void/decline notices and verification codes go out as usual. Their email is still required. Not allowed on cc or in_person_signer (422). Set it on create, on a draft, or with a correction before they act; POST /templates/{id}/envelopes accepts it on each recipients entry. 5. POST /envelopes/{id}/fields. Geometry in PDF points from the page's top-left; page counts within document and may be negative. Only signers and in-person signers have fields. A field needs a recipient unless prefill is true. For a fillable PDF form: GET /envelopes/{id}/form_fields lists its fields (name, type, page, box, value, status assigned/kept/unassigned, recipient) and an acceptance_digest. PUT /envelopes/{id}/form_fields with acceptance_digest and fields [{name, recipient, required?, label?}] chooses which ones recipients fill; every form field left out stays as it is in the PDF. Signature and initials fields can't stay empty. A stale acceptance_digest returns 422. When requires_acceptance is true (signed upload, converted XFA form), the PUT also accepts the report, which sending needs. Templates: GET and PUT /templates/{id}/form_fields with role instead of recipient. 6. POST /envelopes/{id}/preview returns a 15-minute link to the draft watermarked "DRAFT — not for signature". With parent_origin it also returns preview_session for a read-only iframe. 7. POST /envelopes/{id}/send (envelopes:send). From a template: GET /templates/{id} for role labels, then POST /templates/{id}/envelopes with recipients [{role_label, name, email}] (for an in_person_signer role, host_email is required and email optional), optional cc_recipients, values (keyed by field label), signing_order, external_id, name, folder. send defaults to true; send: false keeps a draft. After sending: POST /remind (optional recipient), /extend (days), /void (optional reason; envelopes:void), /duplicate, /retry-finalization; GET /artifacts returns 5-minute download links (artifacts:read); GET /audit_trail returns the certificate's audit entries as data (event, actor, ip_address, occurred). POST /archive and /restore (envelopes:write) hide or bring back an envelope (archived; GET /envelopes?filter[archived]=true lists them; void an envelope out for signature first; an archived draft, like a deleted one, is purged after the workspace's deleted-draft retention). GET /envelopes/{id}, /audit_trail and GET /templates/{id} also return archived ones. Changes only a draft allows (PATCH and DELETE /envelopes/{id}, recipients, fields, documents, form_fields, preview) return 409 resource_conflict on a sent envelope. POST /legal_hold places a hold (legal_hold) that only an owner or admin can release in the app. POST /envelope_downloads with envelopes (up to 50 IDs, artifacts:read) returns a 10-minute link to one ZIP of the completed ones' signed PDFs, plus skipped. Templates: POST /templates from documents (pages of ready files) or from an envelope, then PUT /templates/{id}/roles and POST /templates/{id}/fields (points, page within document, a role or prefill). POST /templates/{id}/preview returns a 15-minute link to a watermarked PDF with each field labeled. POST /templates/{id}/duplicate copies a template; DELETE /templates/{id} archives it, GET /templates?filter[archived]=true lists archived ones and POST /templates/{id}/restore restores one. POST /templates/{id}/share_link (role, welcome_message) and POST /templates/{id}/kiosk (role, welcome_message, pin, idle_timeout_seconds, redirect_delay_seconds) turn them on and return the public url; DELETE turns them off. Template reads show share_link_enabled and kiosk_enabled, never the URL. Folders and filing rules (folders:read, folders:write): GET/POST /folders, GET/PATCH/DELETE /folders/{id} (type envelope or template; parent nests them; PATCH name or parent; DELETE moves the contents up a level). GET/POST /filing_rules, GET/PATCH/DELETE /filing_rules/{id} (name, is_default, targets [{status, folder}] for sent, completed, voided, declined, expired; needs a plan with filing rules). Envelopes and templates have folder; POST /envelopes/{id}/move (envelopes:write) and POST /templates/{id}/move (templates:write) take folder, or null to take it out. A moved envelope is no longer filed by its rule. POST /envelopes/{id}/filing_rule and POST /templates/{id}/filing_rule take filing_rule (frl_…, or null to clear); envelope and template responses show filing_rule. POST /envelopes/{id}/corrections changes an envelope out for signature: recipients (by id, for people who haven't acted; an in-person signer's host_email can change too, except while the device is with them), add_recipients, remove_recipients, documents, fields, note, expected_revision. Send documents and fields in separate requests. Once any recipient has acted, content changes return 409 correction_requires_void. A recipient's signing link stops working only when their email, phone, authentication method or embedded flag changes (turning embedded off emails them a new link); document and field changes keep links and open signing sessions working on the new revision, ask everyone to consent again, and email everyone whose turn it is with their existing link. ## Signing sessions POST /envelopes/{env}/recipients/{rcp}/signing_sessions (envelopes:write) with mode hosted or embedded. embedded requires parent_origin, which must exactly match an origin in the workspace's Settings > Embedding. return_url must be on an allowed return origin (the workspace's signing_return_allowed_origins; platform keys set it with settings.signing_return_allowed_origins on POST/PATCH /workspaces). Optional appearance (primary, radius, font, dark_mode) and actor. The returned url is single-use and must be opened within 10 minutes; the session then lasts 60 minutes idle and 8 hours at most. Only the recipient whose turn it is can get a session, and their authentication still applies. In-person signers can't: their host starts in-person signing from the envelope page in the app. To hold back signing emails for only some recipients, set embedded: true on them instead of send_emails: false. Embedded pages post esignstack:* messages with an event key (ready, page.viewed, consent.accepted, field.completed, recipient.signed, recipient.approved, recipient.acknowledged, recipient.declined, session.expired, error, resize). Messages and redirects are not proof of signing; confirm with GET /envelopes/{id} or a verified webhook. ## Editor sessions POST /editor_sessions with target (a draft env_ or a tpl_), mode, parent_origin (embedded) or return_url (hosted, HTTPS), capabilities (edit_documents, edit_recipients, edit_fields, send) and optional actor. Capabilities can't exceed the key's abilities. Single-use url, open within 10 minutes; 60 minutes idle, 8 hours at most. Embedded sessions post messages with a type key: esignstack:editor.ready, editor.saved, editor.sent, editor.cancelled, resize. Hosted sessions return to return_url?session=...&status=saved|sent|cancelled&target=.... ## Events and webhooks Every change is recorded as an event and kept for 30 days: GET /events (filter[type], filter[object], filter[created][gt|gte|lt|lte], as a date or Unix time) and GET /events/{id}, ability webhooks:read. Manage endpoints with /webhook_endpoints (create, list, retrieve, PATCH, DELETE), POST /webhook_endpoints/{id}/rotate_secret (expire_old_in 0 to 86400), /test and /resend_failed, and GET /webhook_endpoints/{id}/deliveries. POST /events/{id}/resend with webhook_endpoint creates a new delivery. Write operations need webhooks:write. Endpoint URLs must be HTTPS and public; enabled_events takes event types or "*"; include: ["field_values"] adds field values. Deliveries are POSTs with Webhook-Signature: t=,v1= (two v1 values while a rolled secret overlaps), Webhook-Id (the event ID), Webhook-Delivery-Id and Webhook-Attempt. Reject t older than 5 minutes. Respond 2xx within 15 seconds; redirects aren't followed. Connection errors, 408, 409, 425, 429 and 5xx are retried with backoff for up to 3 days. Events can arrive out of order or more than once: de-duplicate on Webhook-Id and fetch the envelope for its current state. An endpoint whose deliveries all fail for 5 days is turned off and webhook_endpoint.disabled is recorded. Event types: envelope.created, envelope.sent, envelope.corrected, envelope.completed, envelope.voided, envelope.declined, envelope.expired, envelope.expiration_extended, envelope.finalization_failed, recipient.activated, recipient.viewed, recipient.consented, recipient.signed, recipient.approved, recipient.acknowledged, recipient.declined, recipient.reassigned, recipient.authentication_failed, recipient.authentication_locked, recipient.delivery_sent, recipient.delivery_bounced, reminder.sent, file.ready, file.failed, template.created, template.updated, template.deleted, workspace.created, workspace.updated, workspace.suspended, webhook_endpoint.disabled. ## Platforms A platform creates one workspace per customer with POST /workspaces and its own external_id, works in it with a platform key plus Workspace-Id or issues workspace keys with POST /api_keys, manages brand kits, and receives every workspace's events on one platform endpoint. With platform:read it can read its own settings (GET /platform) and staff (GET /platform/members); GET /audit_events with audit:read and Workspace-Id reads a workspace's activity log. Staff sign in through login links (POST /login_links) or the platform's identity provider. Full guide: /docs/platform-integration-guide. ## MCP servers Two Model Context Protocol servers over Streamable HTTP. The operations server at /mcp uses the same API keys, abilities, Workspace-Id, rate limits and API envelope limits as REST, with tools list_templates, get_template, create_template, update_template, set_template_roles, get_template_pages, place_template_fields, preview_template, duplicate_template, archive_template, restore_template, set_template_share_link, set_template_kiosk, set_template_documents, create_upload_link, upload_file_from_url, create_envelope_draft, update_envelope_draft, set_recipients, place_fields, get_envelope_pages, list_fields, list_form_fields, assign_form_fields, list_template_form_fields, assign_template_form_fields, preview_envelope, send_envelope, list_envelopes, get_envelope, remind_recipient, extend_envelope, void_envelope, correct_envelope, duplicate_envelope, delete_envelope_draft, retry_finalization, list_events, get_event, download_artifact_link, get_usage, get_platform, list_platform_members, list_folders, create_folder, rename_folder, move_folder, delete_folder, move_envelope, move_template, set_envelope_filing_rule, set_template_filing_rule, list_filing_rules, create_filing_rule, update_filing_rule, delete_filing_rule, get_envelope_audit_trail, list_audit_events, archive_envelope, restore_envelope, place_legal_hold and download_envelopes. No tool signs, approves, declines or consents for a recipient or returns a signing link. send_envelope, void_envelope, delete_envelope_draft, delete_folder, delete_filing_rule and place_legal_hold need confirm=true after the user confirms, as do correct_envelope, set_template_documents when it would remove fields, turning on set_template_share_link or set_template_kiosk, and turning off a kiosk that has a staff PIN; void_envelope needs envelopes:void and takes an optional reason; set_recipients keeps a kept recipient's verification and signing stage (routing_order sets one); set_recipients, correct_envelope, create_envelope_draft and set_template_roles set signer verification as the app and REST do (auth_method none, email_otp, sms with auth_phone on live keys, or access_code with the access_code you give, 4 to 50 characters, which is stored hashed and never returned by any read); get_usage with a platform key also returns the platform's own allowance; for field placement, bring your own AI: get_envelope_pages, preview_envelope, then place_fields; send_envelope, duplicate_envelope, create_upload_link, upload_file_from_url, create_template and correct_envelope accept idempotency_key; for a file on the agent's computer, create_upload_link returns a one-time 15-minute HTTPS link and a curl command (PUT the file as the body, no API key) whose response is the new draft envelope, so never put a document on a public file host to use upload_file_from_url. The docs server at /mcp/docs needs no key and offers search_docs and read_doc. See /docs/mcp. ## Prompt for coding agents Read /docs/api.json before writing code and use only the endpoints it lists. Keep secret keys on the server and give them only the abilities you need. Build against a test key first. Send an Idempotency-Key on every POST and reuse it for retries. Send Api-Version, handle error.code and Retry-After, and ignore unknown fields. Treat an envelope as complete only when the API or a verified webhook says so, never because of a browser message or redirect.