Signing / E-Signature Audit-archive envelopes, PAdES-LT cryptographic signatures, downloadable audit certificates.

Base URL: https://api.fast.io/current/ Sender auth: Bearer {jwt_token} Signer auth: signer session token in URL path Format: JSON

What This Is

A SignEnvelope is an audit-archive Profile holding up to twenty PDFs sent to one or more recipients for electronic signature. Every envelope is parented to a Workspace. The platform's internal PAdES-LT signing engine produces a long-term-validation cryptographic signature on every completed document; an envelope-level audit certificate captures the chain of evidence (consent acceptance, OTP authentication where required, per-recipient sign events, and document hashes). The audit chain is hash-linked and verifiable, and a downloadable audit certificate is available once an envelope reaches a terminal state (completed, declined, voided, expired, or failed).

Each envelope flows through a small lifecycle state machine — Draft, Sent, InProgress, then one of Completed / Declined / Voided / Expired / Failed — and emits a global activity-stream event on every transition so comments and notification subscribers can react.

Permission model: Workspace membership covers read endpoints, /send, document downloads (original and signed PDFs) and the audit-certificate download; workspace admin is required only for /void and /retry. Signer-surface endpoints are authenticated by the path token only.

Endpoint Summary

Sender / Admin Surface — Workspace-Parented Envelopes

MethodEndpointDescription
POST/current/workspace/{workspace_id}/sign_envelopes/create/Create a draft envelope
GET/current/workspace/{workspace_id}/sign_envelopes/list/List envelopes owned by the workspace (offset-paginated)
GET/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/details/Get an envelope (with documents/recipients/fields sub-collections)
POST/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/update/Update mutable fields on a draft envelope (POST or PATCH; POST recommended)
POST/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/send/Send a draft envelope (Draft → Sent; routes to first recipient slot)
POST/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/void/Void a non-terminal envelope (cascade to Voided; reason required)
POST/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/retry/Manually re-drive a stuck envelope through the self-healing recovery routine (admin; idempotent + no-op-success; no body; permanent failures cascade to Failed)
GET/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/documents/{document_id}/download/Stream the source PDF bytes directly (application/pdf; Bearer-authed)
GET/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/documents/{document_id}/preview/Stream the source PDF for inline preview (application/pdf; Bearer-authed)
GET/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/documents/{document_id}/signed/download/Stream the signed PDF bytes directly (application/pdf; Bearer-authed; 404 code 146422 until the document completes)
GET/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/audit/download/Stream the audit-certificate bytes directly (JSON evidence record; Bearer-authed; 404 code 128301 until the envelope reaches a terminal state)
GET/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/audit/pdf/download/Stream the rendered Certificate-of-Completion PDF directly (application/pdf; Bearer-authed; 404 code 121249 until the envelope is terminal and the PDF has rendered)
POST/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/my_sign_link/Mint an action-capable signer link for the calling workspace member when they are themselves a currently-actionable pending signer (else a structured blocked / terminal / reauth response)

Signer Surface (Public — Path Token Auth)

The recipient receives a notification with a signing link of the form /sign_envelopes/signer/{token}/view. The {token} is a short-lived JWT bound to a (sign_envelope_id, recipient_id) pair; no Fastio session is required to call these endpoints.

MethodEndpointDescription
GET/current/sign_envelopes/signer/{token}/view/Landing — returns the envelope, recipient state, document list with per-document source download URLs, the recipient's fields, and the in-force consent disclosure
GET/current/sign_envelopes/signer/{token}/authenticate/Issue an OTP (recipients with auth_method=email_otp or sms_otp)
POST/current/sign_envelopes/signer/{token}/authenticate/Verify an OTP and elevate the session token
POST/current/sign_envelopes/signer/{token}/sign/Submit consent + field values; queues the async PAdES signing job
GET/current/sign_envelopes/signer/{token}/status/Poll the signing pipeline; returns adaptive next_poll_seconds
POST/current/sign_envelopes/signer/{token}/decline/Decline to sign (envelope cascades to Declined)
GET/current/sign_envelopes/signer/{token}/completed/Once the envelope is terminal — recipient state, per-document completion, audit-certificate availability
GET/current/sign_envelopes/signer/{token}/documents/{document_id}/download/Stream the source PDF for one document (original pre-signature file — for the signing UI's field overlay). Signer token is the sole credential
GET/current/sign_envelopes/signer/{token}/documents/{document_id}/signed/download/Stream the signed PDF for one document. Signer token is the sole credential. 1609 (Not Found) until the signed PDF exists; 1680 (Access Denied) / 401 until the envelope is fully completed
GET/current/sign_envelopes/signer/{token}/audit/download/Stream the envelope's audit certificate (JSON). Signer token is the sole credential. 1609 (Not Found) until the certificate exists; 1680 (Access Denied) / 401 while the envelope is still active
GET/current/sign_envelopes/signer/{token}/audit/pdf/download/Stream the rendered Certificate-of-Completion PDF. Signer token is the sole credential. 1680 (Access Denied) / 401 while the envelope is still active; 1609 (Not Found) once terminal but the PDF has not yet rendered

Provider Webhook Surface (Public — HMAC Auth)

Inbound webhook receiver for provider events. The connection is identified by an opaque short id in the URL and is authenticated via the provider's own HMAC signature header (verified per-provider, never via a Fastio session token).

MethodEndpointDescription
POST/current/webhooks/sign_envelopes/{provider}/{connection_short_id}/Receive a provider webhook event (signature-verified, replay-deduped, enqueued for the async normalizer)

{provider} is the slug of the connected provider implementation; the platform ships a Documenso connector for inbound provider integration.

Sign Template Surface — Workspace-Parented Templates

Reusable signing templates that capture recipient role slots, document slots, field placements, and envelope policy. A template is instantiated to produce a draft envelope with concrete bindings applied. Signing is enabled on every plan. Template ids use the sa OpaqueId family (30-character self-describing string).

MethodEndpointDescription
POST/current/workspace/{workspace_id}/sign_templates/create/Create a signing template (PERM_MEMBER)
GET/current/workspace/{workspace_id}/sign_templates/list/List non-deleted templates in the workspace (PERM_VIEW; offset-paginated)
GET/current/workspace/{workspace_id}/sign_templates/{template_id}/details/Fetch a template with full snapshot (PERM_VIEW)
POST/current/workspace/{workspace_id}/sign_templates/{template_id}/update/In-place update under optimistic CAS — expected_version required (PERM_MEMBER)
POST/current/workspace/{workspace_id}/sign_templates/{template_id}/delete/Soft-delete a template — tombstoned, not purged; advisory referrer scan returned (PERM_ADMIN)
POST/current/workspace/{workspace_id}/sign_templates/{template_id}/instantiate/Apply a template: bind role slots to concrete people, resolve document slots, create a draft envelope (PERM_MEMBER)

Availability

Signing is enabled on every plan; capabilities.signing on the org resource confirms availability. Should an org's plan ever not grant signing, the signing endpoints reject calls with a feature-disabled error.

Concepts

SignEnvelope Profile

A SignEnvelope is a Profile — the same hierarchy primitive used by Workspaces and Shares. Each envelope carries:

Documents, recipients, and fields are stored as sub-collections of the envelope and are returned inline on the detail / create response when present.

Lifecycle

draft        -> sent
sent         -> in_progress | declined | voided | expired
in_progress  -> completed | declined | voided | expired | failed
(terminal)   -> nothing

completed, declined, expired, voided, and failed are terminal. Sending a draft transitions draft -> sent and activates the first recipient's slot (sequential routing) or every recipient at once (parallel routing). A single decline cascades the envelope to declined. A void is sender-initiated and cascades the envelope to voided (a reason string is required; non-refundable per industry convention). Expiry runs from the expires_at policy and cascades to expired. failed is reserved for signing-pipeline errors that exhaust retries. A past-deadline envelope that is not yet complete is rejected for signing and declining, and is automatically transitioned to expired in the background; an envelope whose required recipients have all signed is allowed to finish (it is not expired).

Setting envelope_status directly via the update endpoint is not supported; lifecycle transitions only happen through dedicated endpoints (/send, /void) and through the signer-surface actions.

Documents, Recipients, Fields

A draft envelope is created with up to twenty source documents, at least one recipient, and zero or more fields. Documents are referenced by their source_node_id (a node in storage) and optionally a source_version_id (the document resource returns source_version_id: null when no specific version was pinned at create time); the create flow copies the file bytes into the envelope's own private storage instance so the envelope is a self-contained archive. Each document carries a display_order, a source_sha256, a signed_pdf_node_id (populated when signing completes), a completed_sha256, and a signed_at timestamp.

The authoritative “the signed PDF exists and is downloadable” signal for a document is a non-empty signed_pdf_node_id, surfaced as the boolean signed_document_available. The envelope resource rolls this up across all documents: signing_complete is true only once every document has a signed artifact (it is derived from the signed artifacts, so a terminal but unsigned envelope — declined, voided, expired, or failed — is NOT signing_complete), documents_progress reports { total, completed, in_progress, failed } counts (a document is completed when its signed artifact exists), and audit_certificate_available is true once the terminal-envelope audit certificate has been written (the certificate is generated on any terminal outcome — completed, declined, voided, expired, or failed). signing_failed is true when at least one document's signing attempt has errored without yet producing a signed artifact — the failure may be transient (it will retry) or permanent — and is independent of signing_complete; gate downloads on signed_document_available / signing_complete, not on document_copyback_status.

Recipients carry a role (one of signer, cc, viewer, approver, certified_recipient), a routing_order (1-based for sequential routing; identical numbers run in parallel), an auth_method (none, email_otp, sms_otp), and per-recipient lifecycle timestamps. Recipient status flows pendingsentviewedauthenticatedsigning_in_progresssigned, with declined / expired / voided / failed as terminal short-cuts.

Fields are placed on a (document_id, page) using normalized 0..1 coordinates (x_norm / y_norm for the top-left corner, w_norm / h_norm for the bounding box). The supported field type values are signature, initial, date, text, and checkbox. A field belongs to exactly one recipient and is rendered only when that recipient signs.

A field can optionally carry a validation object that constrains the value a signer may submit. It is echoed back as validation_json on the field resource (the /details/ and signer /view/ responses) so the signing UI can enforce the same rules client-side. Supported keys (all optional):

Any other key, or a validation object on a signature / initial / checkbox field, is rejected at create/update time with 1605 (Invalid Input). The object is also size-capped. At /sign/ time the platform enforces these rules against the submitted value before recording the signature; a violation is rejected with 1605 (Invalid Input) and lists the offending fields. Date fields are always validated as real calendar dates at sign time even without a validation object.

Trust Class

Every envelope-resource response carries a read-only _trust_class field set to system_trusted. Handler outputs that pass through a SignEnvelope are platform-derived and downstream consumers (audit pipelines, AI agents) can rely on the value without independent verification. _trust_class is display metadata; it is never accepted as an input.

Audit Chain & Certificate

Every state change on an envelope appends a hash-chained audit row. Rows record the envelope's event_seq, an event_type (see Activity Events below), the actor's identifier, and a content_hash that links to the immediately preceding row's hash. The chain is signed by a per-workspace HMAC audit key.

When an envelope reaches a terminal state — completed, declined, voided, expired, or failed — the platform builds a structured audit certificate (JSON evidence record with a self-anchoring HMAC) and copies it into the envelope's storage instance. The audit_certificate_node_id field on the envelope resource carries the node reference. The dedicated /audit/download/ endpoint streams the certificate bytes directly (Content-Disposition: attachment, no token round-trip). Until the certificate exists, the owner endpoint returns 404 with code 128301 and the signer endpoint returns 1609 (Not Found) — so a voided envelope's certificate IS downloadable; do not poll “until completed”.

The certificate is a single JSON object with these top-level keys: schema_version ("1.0"), generated_at, envelope (id, status, lifecycle timestamps, revision_number, first-view charge state, and parent{parent_id,parent_type}), recipients[] (id, signer identity, email, display name, role, routing order, status, auth method, per-stage timestamps, decline reason), documents[] (document_id, display_order, source_node_id, source_sha256, signed_pdf_node_id, completed_sha256, signed_at, copyback_status), consent_texts[] (version, body_hash), audit_chain[] (hash-linked rows: sequence, event_id, event_type, actor ids, occurred_at/recorded_at, prior_event_hash/event_hash, ip, user_agent, document_hash, consent_text_version, provider ids, and canonical_json), audit_chain_count, and attestation{algorithm:"HMAC-SHA256", master_version, attestation_hmac_sha256} anchoring the whole body. All timestamps are Y-m-d H:i:s UTC; all hashes are 64-character lowercase hex. The JSON is the machine-verifiable authoritative record; a human-readable Certificate-of-Completion PDF rendition of the same evidence is also produced and downloadable (see Certificate-of-Completion PDF below).

Internal PAdES-LT Signing

The default signing path uses the platform's internal PAdES-LT engine — a long-term-validation cryptographic signature embedded directly in each completed PDF. Signed PDFs include an embedded validation certificate chain so the signature remains verifiable offline without contacting the signing service. The signing pipeline runs asynchronously after /sign; the signer client polls /status/ for completion.

Recipient Authentication (OTP)

When a recipient's auth_method is email_otp or sms_otp, the recipient must complete an OTP gate before submitting field values. The flow is:

  1. The recipient's client calls GET /sign_envelopes/signer/{token}/authenticate/ — the platform issues a fresh 6-digit code and sends it to the recipient. The code is short-lived and code-issuance is throttled per (envelope, recipient).
  2. The recipient submits the code via POST /sign_envelopes/signer/{token}/authenticate/. On match, the platform records the authenticated timestamp, re-mints the session token with the elevated auth_level, and returns the new token. On miss, the response includes attempts_remaining and verification is throttled per (envelope, recipient).
  3. The recipient's subsequent /sign/ call carries the elevated token; the platform refuses to accept signatures from a token whose auth_level does not satisfy the recipient's configured auth_method.

Recipients with auth_method=none skip the authenticate step entirely.

A recipient who already authenticated but no longer holds the elevated token — for example a returning signer who reopened the email link in a fresh browser session — may re-invoke GET /sign_envelopes/signer/{token}/authenticate/ to receive a new code and re-elevate. The prior authentication is preserved (re-issuing a code does not reset recipient state); only signed, declined, and otherwise-terminal recipients are refused.

Before submitting field values, a recipient must accept the in-force ESIGN/UETA consent disclosure surfaced on the /view response. The acceptance is recorded in the audit chain along with the hash of the consent body the recipient was shown. The /sign/ request carries consent_body_hash and consent_text_version to prove the acceptance; a mismatch is rejected with 1605 (Invalid Input).

Document Byte Access

The source document download and preview endpoints stream the PDF bytes directly (200 application/pdf, Bearer-authed) — the bytes live in the envelope's own storage instance and are fetched server-side, so a document's source_node_id must NOT be read through the generic Storage /storage/{node_id}/read/ endpoint (that resolves nodes in the workspace instance, not the envelope's). The signed-PDF (signed/download) and audit-certificate (audit/download) endpoints stream their bytes the same way (signed PDF as application/pdf; the audit certificate as its JSON evidence record).

First-View Billing

Sending an envelope reserves credits at send time; a separate first-view charge fires the first time any recipient opens the /view landing page. Subsequent views by the same or different recipients do not re-charge — the billing meter is once-per-envelope. The user-visible first_view_charged_at timestamp is returned on the envelope resource so callers can see when the meter fired. Voiding an envelope does not refund the credits (industry convention: credits consumed at send).

Request / Response Shapes

SignEnvelope Resource

{
  "result": true,
  "sign_envelope": {
    "id": "{envelope_id}",
    "_trust_class": "system_trusted",
    "name": "Master Services Agreement",

    "envelope_status": "draft",
    "is_terminal": false,

    "signing_complete": false,
    "signing_failed": false,
    "documents_progress": { "total": 1, "completed": 0, "in_progress": 1, "failed": 0 },
    "audit_certificate_available": false,
    "audit_certificate_pdf_available": false,

    "provider": "internal_pades",
    "provider_envelope_id": "{provider_envelope_id}",
    "revision_number": 1,
    "origin_step_occurrence_id": null,

    "sent_at": null,
    "completed_at": null,
    "voided_at": null,
    "voided_reason": null,
    "expires_at": "2026-06-15 14:30:00 UTC",

    "first_view_charged_at": null,
    "audit_certificate_node_id": null,
    "audit_certificate_pdf_node_id": null,

    "policy": {
      "auth_method": "email_otp",
      "reminder_cadence_hours": 24,
      "retention_days": 2555
    },

    "archived": false,
    "closed": false,
    "locked": false,
    "suspended": false,
    "legal_hold": false,

    "parent_type": "workspace",
    "parent_workspace": {
      "id": "12345678901234567890",
      "name": "Sales Contracts",
      "parent_org_id": "98765432109876543210"
    },
    "parent_org": "98765432109876543210",

    "created_at": "2026-05-23 17:42:18 UTC",
    "updated_at": "2026-05-23 17:42:18 UTC",
    "deleted_at": null,
    "retention_expires_at": null,

    "documents": [
      {
        "document_id": "{document_id}",
        "source_node_id": "2ltsu-q4mja-cuv7p-gc5yd-lxnsj-wee4",
        "source_version_id": "3l5np-obens-xscsb-y5wws-gwcd6-ni6t",
        "source_sha256": "{hex_sha256}",
        "display_order": 0,
        "signed_pdf_node_id": null,
        "completed_sha256": null,
        "completed_byte_count": null,
        "signed_at": null,
        "document_copyback_status": "not_required",
        "signed_document_available": false,
        "created_at": "2026-05-23 17:42:18 UTC",
        "updated_at": "2026-05-23 17:42:18 UTC"
      }
    ],

    "recipients": [
      {
        "recipient_id": "{recipient_id}",
        "signer_identity_id": "{signer_identity_id}",
        "email": "signer@example.com",
        "display_name": "Alex Signer",
        "role": "signer",
        "routing_order": 1,
        "status": "pending",
        "auth_method": "email_otp",
        "sent_at": null,
        "viewed_at": null,
        "authenticated_at": null,
        "signed_at": null,
        "declined_at": null,
        "decline_reason": null,
        "created_at": "2026-05-23 17:42:18 UTC",
        "updated_at": "2026-05-23 17:42:18 UTC"
      }
    ],

    "fields": [
      {
        "field_id": "{field_id}",
        "recipient_id": "{recipient_id}",
        "document_id": "{document_id}",
        "page": 1,
        "x_norm": 0.5,
        "y_norm": 0.5,
        "w_norm": 0.2,
        "h_norm": 0.05,
        "type": "signature",
        "required": true,
        "value_json": null,
        "validation_json": null,
        "created_at": "2026-05-23 17:42:18 UTC",
        "updated_at": "2026-05-23 17:42:18 UTC"
      }
    ]
  }
}

Create an Envelope

curl -X POST "https://api.fast.io/current/workspace/{workspace_id}/sign_envelopes/create/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Master Services Agreement",
    "expires_at": "2026-06-15 14:30:00 UTC",
    "policy_json": {
      "auth_method": "email_otp",
      "reminder_cadence_hours": 24
    },
    "documents": [
      {
        "source_node_id": "2ltsu-q4mja-cuv7p-gc5yd-lxnsj-wee4",
        "source_version_id": "3l5np-obens-xscsb-y5wws-gwcd6-ni6t",
        "display_order": 0
      }
    ],
    "recipients": [
      {
        "email": "signer@example.com",
        "display_name": "Alex Signer",
        "phone_e164": "+15555550123",
        "role": "signer",
        "routing_order": 1,
        "auth_method": "email_otp"
      }
    ],
    "fields": [
      {
        "recipient_email": "signer@example.com",
        "document_index": 0,
        "page": 1,
        "x_norm": 0.5,
        "y_norm": 0.5,
        "w_norm": 0.2,
        "h_norm": 0.05,
        "type": "signature",
        "required": true,
        "value_json": null
      },
      {
        "recipient_email": "signer@example.com",
        "document_index": 0,
        "page": 1,
        "x_norm": 0.5,
        "y_norm": 0.6,
        "w_norm": 0.3,
        "h_norm": 0.04,
        "type": "text",
        "required": true,
        "validation": { "min_length": 2, "max_length": 64, "pattern": "[A-Za-z .'-]+" }
      }
    ]
  }'

Body fields:

A successful create returns 200 OK with the envelope in envelope_status: "draft" as a FLAT SignEnvelope object — the documents / recipients / fields sub-collections are NOT inlined on the create response (only /details/ inlines them), and provider / provider_envelope_id are null (the signing provider is allocated at send time). Fetch /details/ after create to read the generated document_ids and recipient ids. The envelope sits in draft until /send/ is invoked.

Update an Envelope

curl -X POST "https://api.fast.io/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/update/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Master Services Agreement (rev B)",
    "documents": [
      { "id": "{existing_document_id}", "display_order": 0 },
      { "source_node_id": "{new_pdf_node_id}", "display_order": 1 }
    ],
    "recipients": [ ... ],
    "fields": [ ... ]
  }'

Update accepts POST or PATCH (POST recommended — PATCH is accepted by the endpoint but some intermediaries block it). Only draft envelopes are editable (a non-draft envelope is rejected with 1680 (Access Denied) / HTTP 401). Each successful update increments revision_number and re-appends the draft audit row. Field semantics: name (optional) — renames the envelope when a non-blank string is sent; absent / null / blank leaves the current name unchanged (the name cannot be cleared via update). expires_at and policy_json are declarative — every update writes them, so OMITTING one CLEARS it (resets to null); resend the current values to keep them. recipients is required and always a full replacement (>= 1). documents and fields are optional — omit to keep the current set. When you omit fields, the existing placements are preserved and re-mapped onto the recipients you send (matched by email; the endpoint regenerates recipient ids, so placements are re-pointed for you), and any placement whose signer or document was removed is dropped; supply a non-empty fields to replace all placements, or an explicit empty fields: [] to clear them. documents is optional:

Send an Envelope

curl -X POST "https://api.fast.io/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/send/" \
  -H "Authorization: Bearer {jwt_token}"

/send/ runs a billing preflight, transitions draft -> sent, activates the first routing-order slot (sequential) or every recipient (parallel), appends an audit row, and dispatches the notification pipeline. The response carries the post-send envelope shape. An already-sent envelope returns success without re-emitting events (idempotent retry semantics).

/send/ takes no body — it sends the envelope's already-saved documents, recipients, and fields. Before sending, the envelope must be complete: at least one document, at least one signing recipient (a signer, approver, or certified_recipient — a draft with only cc/viewer recipients cannot be sent), at least one field, and every signer-role recipient must have at least one field to complete (cc/viewer roles do not need a field). These checks run only at send time, not on create/update, so an in-progress draft may be saved incomplete. If the envelope is not ready, /send/ returns an input-validation error whose message names the specific gap — e.g. “Recipient {email} is a signer but has no fields to complete” — so you can fix exactly that recipient (place a field, change their role, or remove them) and retry.

Void an Envelope

curl -X POST "https://api.fast.io/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/void/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Counterparty no longer required"}'

reason is required and capped at 1024 bytes. Void is destructive — voiding an already-terminal envelope (or any other disallowed transition) is rejected with 1680 (Access Denied) / HTTP 401; non-terminal envelopes cascade to voided with all pending recipients short-circuited. Credits are not refunded.

curl -X POST "https://api.fast.io/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/my_sign_link/" \
  -H "Authorization: Bearer {jwt_token}"

Mints an action-capable signer link for the calling workspace member when that member is themselves a currently-actionable pending signer on the envelope — the only supported way for a workspace member who is also a recipient to obtain a sign link. The caller's recipient row is resolved through the platform's fail-closed, verified-email, org-scoped identity resolution, so a member can never obtain a link bound to a recipient that is not provably theirs.

The endpoint is POST (not GET) and takes no body: minting a signer token and consuming a throttle slot are state-changing, and declaring POST makes the endpoint enforce write scope — a read-only-scoped API token cannot acquire an action-capable sign link. Requires workspace membership on the envelope and the org's signing capability; both are checked before any token is minted, and the request is rate-limited per (member, envelope).

The response always returns 200 with result: true and a decision-shaped body; the decision is conveyed by which fields are populated:

{
  "result": true,
  "sign_url": null,
  "recipient_status": "pending",
  "auth_method": "email_otp",
  "routing_order": 2,
  "blocked_signers": [
    {"display_name": "Jordan Counterparty", "routing_order": 3}
  ],
  "is_terminal": false
}

If the caller is not a signer on the envelope, the request is rejected with 1680 (Access Denied) / HTTP 401 (no recipient detail is leaked). Exceeding the per-(member, envelope) request throttle also returns 1680 (Access Denied) / HTTP 401.

Signer View

curl -X GET "https://api.fast.io/current/sign_envelopes/signer/{signer_token}/view/"

Returns the envelope state, the recipient's own state, the list of documents (each carrying an absolute, cookie-free download_url — served from the downloadai content host so the signer's app cookies are never attached — the client fetches directly to stream the source PDF), the fields belonging to this recipient, and the in-force consent disclosure with body and body hash. The first call additionally fires the first-view billing meter (best-effort; the view is served even on a billing miss).

{
  "result": true,
  "envelope": {
    "id": "{envelope_id}",
    "envelope_status": "sent",
    "is_terminal": false,
    "expires_at": "2026-06-15 14:30:00 UTC",
    "sender_name": "Sales Contracts",
    "document_count": 1,
    "recipient_count": 1,
    "policy": {
      "otp_required": true,
      "auth_method": "email_otp"
    }
  },
  "recipient": {
    "id": "{recipient_id}",
    "email": "signer@example.com",
    "display_name": "Alex Signer",
    "role": "signer",
    "routing_order": 1,
    "status": "viewed",
    "auth_level": "none",
    "sent_at": "2026-05-23 17:42:18 UTC",
    "viewed_at": "2026-05-23 18:01:04 UTC",
    "authenticated_at": null,
    "signed_at": null,
    "declined_at": null
  },
  "documents": [
    {
      "document_id": "{document_id}",
      "display_name": "Master Services Agreement.pdf",
      "byte_count": 184302,
      "display_order": 0,
      "signed_at": null,
      "download_url": "https://downloadai.fast.io/api/current/sign_envelopes/signer/{token}/documents/{document_id}/download/"
    }
  ],
  "fields": [
    {
      "field_id": "{field_id}",
      "document_id": "{document_id}",
      "page": 1,
      "x_norm": 0.5,
      "y_norm": 0.5,
      "w_norm": 0.2,
      "h_norm": 0.05,
      "type": "signature",
      "required": true,
      "value_json": null,
      "validation_json": null
    }
  ],
  "consent": {
    "version": "1.0.0",
    "body": "...",
    "body_hash": "{hex_sha256}"
  }
}

Signer Authenticate (OTP)

# Issue a code
curl -X GET "https://api.fast.io/current/sign_envelopes/signer/{signer_token}/authenticate/"

# Verify a code; on success returns a new elevated token
curl -X POST "https://api.fast.io/current/sign_envelopes/signer/{signer_token}/authenticate/" \
  -H "Content-Type: application/json" \
  -d '{"code": "123456"}'

The success response carries {"authenticated": true, "new_token": "{signer_token}"} — the client persists the new token and uses it on the subsequent /sign/ call. The failure response carries {"authenticated": false, "attempts_remaining": 2} (a small integer counting down the remaining verify attempts in the current (envelope, recipient) window; the ceiling is not published).

Signer Sign

curl -X POST "https://api.fast.io/current/sign_envelopes/signer/{signer_token}/sign/" \
  -H "Content-Type: application/json" \
  -d '{
    "consent_body_hash": "{hex_sha256}",
    "consent_text_version": "1.0.0",
    "fields": [
      {"field_id": "{field_id}", "value_json": "{\"kind\": \"signature\", \"data_url\": \"data:image/png;base64,{base64_png}\"}"}
    ]
  }'

Returns 202 ACCEPTED with {"status": "signing", "poll_url": "...", "status_token": "{signer_token}", "is_last_signer": false}. The platform queues an asynchronous PAdES-LT signing job and the client polls /status/ until completion. Each issued signer token is single-use for the consume-style actions (/sign, /decline, /authenticate POST) — once consumed the token can no longer drive those actions, and the response surface re-mints a fresh polling-scoped token for the post-sign status flow.

Before the signature is recorded, every submitted value is validated against its field's validation rules (and every date field is validated as a real YYYY-MM-DD calendar date). A value that violates a rule — too short / too long, failing the pattern, outside the date_min / date_max window, or an unparseable date — is rejected with 1605 (Invalid Input), and the error lists each offending field. Because this check runs before the single-use token is consumed, a correctable mistake leaves the link usable for a retry.

Signer Status

curl -X GET "https://api.fast.io/current/sign_envelopes/signer/{signer_token}/status/"

Returns envelope_status, is_terminal, the polling signer's own recipient status, signing_complete, signing_failed, a documents_progress rollup ({ total, completed, in_progress, failed }), and next_poll_seconds. signing_complete is true only once every signed artifact actually exists (a non-empty signed_pdf_node_id on each document) — it is derived from the signed artifacts, never from document_copyback_status, so a terminal but unsigned envelope is NOT signing_complete. next_poll_seconds is 0 only when the envelope is in a terminal state or signing_complete is genuinely true; otherwise it is an adaptive hint (2s for the first 10s after sign, 5s for the next 60s, 15s thereafter). Keep polling while next_poll_seconds is greater than 0, and use a terminal envelope_status as the stop signal: signing_failed indicates a document failed signing (which may be transient — it will retry — or permanent) and does NOT by itself mean polling should stop. The polling-scoped token is multi-call; the original landing token also works until it has been consumed by a one-shot action.

Signer Decline

curl -X POST "https://api.fast.io/current/sign_envelopes/signer/{signer_token}/decline/" \
  -H "Content-Type: application/json" \
  -d '{"reason": "I do not have authority to sign this document"}'

A single decline cascades the envelope to declined. The decline reason is captured in the audit chain. reason is capped at 1024 bytes.

Document Downloads

The source download and preview endpoints stream the PDF bytes directly (200 application/pdf, Bearer-authed); fetch with the Authorization header and render the resulting blob (the URL cannot be a bare <iframe src> — the navigation carries no auth header). The signed-PDF endpoint streams the signed artifact the same way (200 application/pdf, Bearer-authed, 404 until the document completes):

# Source PDF (the file uploaded at create time)
curl -X GET "https://api.fast.io/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/documents/{document_id}/download/" \
  -H "Authorization: Bearer {jwt_token}"

# Source PDF preview (browser-renderable)
curl -X GET "https://api.fast.io/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/documents/{document_id}/preview/" \
  -H "Authorization: Bearer {jwt_token}"

# Signed PDF (404 until the document completes)
curl -X GET "https://api.fast.io/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/documents/{document_id}/signed/download/" \
  -H "Authorization: Bearer {jwt_token}"

The owner signed-PDF endpoint returns 404 with code 146422 (“Signed PDF is not yet available for this document.”) until the document's signed_at timestamp is populated. (The signer-surface twin uses the generic 1609 instead.) Do NOT read a document's source_node_id through the generic /storage/{node_id}/read/ endpoint — that node lives in the envelope's own storage instance, not the workspace tree, so it returns not-found; always fetch bytes through the envelope endpoints above.

Audit Certificate Download

curl -X GET "https://api.fast.io/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/audit/download/" \
  -H "Authorization: Bearer {jwt_token}"

Streams the envelope's audit-certificate bytes directly (JSON evidence record; Bearer-authed). The endpoint returns 404 with code 128301 until the envelope reaches a terminal state (completed, declined, voided, expired, or failed) and the certificate has been rendered and copied back into the envelope's storage instance. The JSON is the machine-verifiable authoritative record; a human-readable Certificate-of-Completion PDF rendition of the same evidence is also produced and downloadable.

Certificate-of-Completion PDF

Alongside the JSON audit certificate, the platform renders a human-readable Certificate-of-Completion PDF — a legally-meaningful summary of the envelope's evidence (recipients, per-stage timestamps, document hashes, consent acceptance) — and copies it into the envelope's own storage instance. It is a first-class download surface on both the owner and signer sides.

# Owner (Bearer-authed)
curl -X GET "https://api.fast.io/current/workspace/{workspace_id}/sign_envelopes/{envelope_id}/audit/pdf/download/" \
  -H "Authorization: Bearer {jwt_token}"

# Signer (path-token authed — no Fastio session)
curl -X GET "https://api.fast.io/current/sign_envelopes/signer/{signer_token}/audit/pdf/download/"

Both endpoints stream the PDF bytes directly (200, application/pdf, Content-Disposition: attachment); there is no read-token round-trip, and the bytes are fetched server-side from the envelope's storage instance (a audit_certificate_pdf_node_id must NOT be passed to /storage/{node_id}/read/). Availability:

The envelope resource surfaces the PDF's availability through audit_certificate_pdf_available (boolean — true once the rendered PDF exists) and audit_certificate_pdf_node_id (the storage node reference, null until rendered) — mirroring the existing audit_certificate_available / audit_certificate_node_id pair for the JSON certificate. The signer /completed/ response also returns an audit_certificate_pdf block — {"available": true, "download_url": "..."} — alongside the JSON audit_certificate block (both are null until their artifact exists).

Plan Availability

Signing is enabled on every plan; capabilities.signing on the org resource confirms availability. Should an org's plan ever not grant signing, new envelopes cannot start or advance; any in-flight envelopes drain to a terminal state.

Sign Templates

What Sign Templates Are

A sign template (SignTemplate) is a workspace-scoped reusable signing configuration identified by a 30-character sa-prefixed OpaqueId. It captures four things: recipient role slots (who must sign, in what role, with what authentication), document slots (which PDFs are needed and whether they are pinned to specific storage nodes or supplied at instantiation time), field placements (where the signature boxes and other fields sit on each document page in normalized 0…1 coordinates), and envelope policy (default message, reminder cadence, etc.).

Instantiating a template binds each slot to a concrete value and produces a draft SignEnvelope that then follows the normal envelope lifecycle. Templates are soft-deleted (tombstoned) when removed.

Snapshot Structure

The snapshot object has four required top-level keys:

Resource Shape

List returns identity fields only (id, name, description, version, author_user_id, created_at, updated_at, deleted_at) — snapshot omitted for size. Create, details, update, and instantiate return the full resource including snapshot. Timestamps use 'Y-m-d H:i:s UTC' format.

Endpoints

Create — POST /current/workspace/{workspace_id}/sign_templates/create/

Body: name (required, max 255 chars), optional description (max 1024 chars), snapshot (required, full object). Returns {"sign_template": {...}} with full snapshot. Error codes: 156113 (invalid input), 150581 (pinned doc not found), 146058 (duplicate).

List — GET /current/workspace/{workspace_id}/sign_templates/list/

Query params: offset (default 0), limit (default 50, max 200). Returns {"sign_templates": [...], "total": int, "offset": int, "limit": int}. Each item is identity-only (no snapshot). Error codes: 148805 / 186446 (invalid params).

Details — GET /current/workspace/{workspace_id}/sign_templates/{template_id}/details/

Returns full resource with snapshot. Error codes: 138322 (invalid template id in URI), 129325 (not found).

Update — POST /current/workspace/{workspace_id}/sign_templates/{template_id}/update/

Optimistic CAS. Body: expected_version (required, integer ≥ 1 — the version you fetched), optional name (absent = preserved), optional description (absent = preserved), optional snapshot (always a full replacement when present). Returns full resource. Error codes: 135997 (invalid id in URI), 174911 (expected_version missing/invalid), 147321 (version conflict — HTTP 409; reload and retry), 156706 (invalid input), 194205 (template not found or pinned doc not found).

Delete — POST /current/workspace/{workspace_id}/sign_templates/{template_id}/delete/

Soft-delete only (tombstoned, not purged). Returns {"sign_template_id": "sa...", "deleted": true, "referrer_count": int, "referrers": [...]}. Advisory only — delete is never blocked by referrers. Error codes: 199127 (invalid id in URI), 112441 (not found).

Instantiate — POST /current/workspace/{workspace_id}/sign_templates/{template_id}/instantiate/

Binds slots to concrete values and creates a draft SignEnvelope. Body: recipient_bindings (required — a map keyed by slot_key string; each value carries email, optional display_name, optional auth_method; a list/array is rejected with recipient_bindings_invalid), optional documents (list to supply or override document slots not pinned in the template; each entry: document_slot_index, source_node_id, optional source_version_id), optional name.

Returns {"sign_envelope": {...}, "geometry_mismatch": bool, "geometry_details": {}}. A geometry_mismatch: true is a soft advisory — the draft is returned alongside the flag and is usable, but field placements may not align visually.

Key error codes: 133174 (invalid id in URI), 101088 (template not found), 127674 (recipient slot unbound), 191903 (document slot unresolved), 104351 (duplicate email), 151857 (auth downgrade rejected), 149201 (auth method not in allowlist), 114536 (doc not found in storage), 188340 (snapshot corrupt).

Webhooks

Provider Webhook Receiver

The platform ships a Documenso connector for inbound provider webhooks. The receiver path is:

POST /current/webhooks/sign_envelopes/documenso/{connection_short_id}/

The {connection_short_id} identifies the configured provider connection (opaque short id). The provider signs each request with its own HMAC scheme; the verifier is per-provider and runs before the body is JSON-parsed. After verification the platform normalizes the event into an internal shape, deduplicates against an idempotency key derived from (connection_id, provider_event_id), and enqueues the work for asynchronous processing.

Receiver semantics:

The raw request body bytes are what the provider signs; do not JSON-parse before verifying. Duplicate deliveries (same (connection_id, provider_event_id) pair) 200-ACK without enqueuing.

Activity Events

Every state transition emits a platform activity-stream event that is observable via the standard Events surface (/current/events/search/). Each transition also publishes to the realtime activity pipeline for live UI updates — see Realtime Activity below.

Event TypeEmitted When
sign_envelope_draftedA draft envelope is created
sign_envelope_sentA draft envelope transitions to sent
sign_envelope_voidedAn envelope transitions to voided
sign_envelope_viewedA recipient opens the /view landing for the first time (first-view billing fires alongside)
sign_envelope_recipient_signedA recipient finishes the /sign flow and the platform commits the recipient transition to signed
sign_envelope_recipient_declinedA recipient declines via /decline
sign_envelope_document_signedThe async PAdES pipeline completes a single document's signature embedding and copyback
sign_envelope_completedThe last document completes and the envelope transitions to completed (audit certificate rendered)
sign_envelope_expiredAn envelope's signing deadline (expires_at) passed while not all required recipients had signed; the envelope is no longer signable and was transitioned to the terminal expired state (outstanding recipients are marked expired and an audit certificate is generated)
sign_envelope_failedAn envelope could not finish its signing pipeline after every required recipient had signed (a permanent document error, or recovery did not finalize within the bounded window) and was transitioned to the terminal failed state (an audit certificate is generated)

Audit-chain rows are written for every transition above plus per-recipient viewed, authenticated, consent_accepted, and an internal first_view_billed row. The audit-chain rows are visible inside the audit certificate (JSON) and are not exposed through a separate event-log endpoint at this version.

Realtime Activity

The transitions above also publish to the realtime activity pipeline so a sender can watch an envelope live instead of polling. The generic activity/websocket model (the activity vs enriched event frames, the activity-field nudge, and the GET /current/activity/poll/{profile_id}/ long-poll fallback) is documented in the Events reference. Signing specifics:

Error Responses

Common error codes for the signing category (see the overview reference for the full code table):

Reading the error tables: the four-digit 16xx/17xx values below are HTTP-status classes, not error.code. The error.code a client actually receives is assigned per endpoint, so use the HTTP status as the gate and a documented error.code — five or six digits, plus the 9661-9669 family — only as a refinement. A 16xx value identifies the status class — useful for telling which kind of failure occurred — but comparing one against error.code will never match. Five- and six-digit codes (and the 9661-9669 family) are real error.code values. If you widen a check from a specific code to a status, widen what you assert with it — a status covers failures the narrower code did not, so a message written for that one code becomes a confident falsehood on the rest.

Error CodeHTTP StatusCause
1605 (Invalid Input)406Field validation failure — bad coordinates, missing required field at sign time, malformed payload, consent body hash mismatch
1609 (Not Found)404Envelope / document / recipient row not found, or signed PDF / audit certificate requested before the envelope reaches that stage
1650 (Authentication Invalid)401Missing / invalid bearer token, signer-surface session-token verification failure, signer token already consumed by a one-shot action, OTP attempts exhausted
1658 (Not Acceptable)406Duplicate envelope row rejected
1671 (Rate Limited)429Standard rate limit; OTP send/verify throttle is reported through the same error
1680 (Access Denied)401Caller lacks the required workspace permission for the action, or a lifecycle transition is not allowed from the current state (editing a non-draft envelope, voiding a terminal envelope, or any other disallowed transition)
1696 (Credit Limit Exceeded)402Org has insufficient signing credits for a /send call
1698 (Unprocessable Entity)422Schema-level rejection on update / payload structurally invalid

The signing service surfaces a richer internal taxonomy that maps onto the codes above. The case callers see most often in practice is the idempotent retry: a second /send/ call against an already-sent envelope returns success without re-emitting events or re-charging credits.

Standard rate limits apply. See the overview for the global rate-limit header schema (x-ve-limit-avail, x-ve-limit-max, x-ve-limit-expires) and error code 1671.

Identifier Formats

Signing API responses carry four identifier kinds. Every identifier is returned as a JSON string — never a number.

Identifier kindShapeFields
Profile id19-digit numeric string (e.g. 4011234567890123456)envelope_id, workspace_id, org_id, every *_user_id field
OpaqueId34-character hyphenated base32 string (e.g. 2ltsu-q4mja-cuv7p-gc5yd-lxnsj-wee4)document_id, recipient_id, signer_identity_id, field_id, source_node_id, signed_pdf_node_id, audit_certificate_node_id, provider_envelope_id, connection_short_id, origin_step_occurrence_id
Sign template id30-character base32 string with the sa family prefix (e.g. sa…)template_id, sign_template_id on instantiate output
Signer tokenCompact JWT carried in the URL path{token} placeholder above — never persist a signer token longer than the envelope's lifetime, and treat it as a single-use bearer for the consume-style actions (/sign, /decline, /authenticate POST)

OpaqueIds are also accepted in an un-hyphenated 29-character form — the router strips hyphens before validation, so a 34-char 2ltsu-q4mja-cuv7p-gc5yd-lxnsj-wee4 and a 29-char 2ltsuq4mjacuv7pgc5ydlxnsjwee4 route identically.

Notes

↑ Back to top