{
  "openapi": "3.1.0",
  "info": {
    "title": "wesign.now API",
    "version": "2026-10-10",
    "summary": "Send PDFs and locked templates for e-signature, poll status, fetch sealed PDFs and audit trails, subscribe to webhooks.",
    "description": "wesign.now (formerly letssign.now) e-signature API, v1. It sends a PDF (POST /signing-requests) or a document filled from a locked template (POST /templates/{id}/instantiate) to one or more signers. Each signer gets a personal signing link — emailed by us, delivered by you (`send_emails: false`), or opened inside your page (POST /embedded/sign-sessions); a per-signer SMS code makes the signature AES. When every signer has signed, the sealed PDF is at GET /documents/{id}/signed and the audit trail is a separate PDF at GET /documents/{id}/audit-trail. Each step is reported through HMAC-signed webhooks.\n\nSTART HERE. (1) GET /me checks the key and reports `capabilities` (branch on those, never on the tier name) and `sms_allowance`. (2) Send a PDF with POST /signing-requests. To fill a template, GET /templates/{id} returns its input contract (`recipients[]` slots with their role and pin, `field_values[]` with `required`, `owner`, `labels`); POST /templates/{id}/instantiate with `validate_only: true` reports what the real call would refuse (`problems`) and what you could still fill (`missing_optional`) and creates nothing; then send the same body without it. Send an `Idempotency-Key` on POST /signing-requests and on instantiate. (3) Register a receiver once with POST /hooks (`payload: \"minimal\"` for identifiers only, no personal data; the `secret` is returned only in that response). Verify `X-WeSign-Signature` as `x-webhook-signature` describes, deduplicate on `event_id` (`document.completed` also on `document_id`), do not rely on delivery order, and answer 2xx within 10 s; POST /hooks/{id}/test sends a signed ping. (4) On `document.completed`, fetch `signed_pdf_url` and `audit_trail_url` with the same key, compare the event's `sha256` with the PDF bytes, and store both on your side: a workspace can have signed PDFs deleted after a set number of days (then `/signed` answers 410), and no endpoint lists documents, so keep every `document_id` your create calls return. Guide: https://www.wesign.now/docs/documents#retrieve-the-signed-document.\n\nNO SANDBOX. Every key is live: every call creates real documents and sends real email and SMS. To test, use `validate_only: true`, or `review: true` and then POST /documents/{id}/discard (template routes); `send_emails: false`; and addresses you control.\n\nDOCS. https://www.wesign.now/docs; for AI assistants https://www.wesign.now/llms.txt (index) and https://www.wesign.now/llms-full.txt (every page as Markdown); the Integrator FAQ https://www.wesign.now/docs/integrator-faq. There is no SDK and no MCP server: generate a client from this document.\n\nHOSTS, KEYS AND LIMITS. The canonical host is https://api.wesign.now/v1; https://api.letssign.now/v1 is a permanent alias that serves the same routes. Every request carries `Authorization: Bearer <api key>` — keys are minted in Settings → API and look like `wsk_live_<32 hex>` (keys minted before the wesign.now rename look like `lsk_live_<32 hex>` and are accepted forever). Errors are JSON `{ error, code, meta? }` with a stable `code`. A key may additionally be restricted to a list of caller IP addresses/CIDR blocks (Settings → API); when that list is non-empty, calls from any other address answer 403 `ip_not_allowed` with the observed address in `ip`. An EMPTY list means UNRESTRICTED — the default, and the state of every key that has not opted in. Every authenticated route except `GET /me` is rate-limited to 60 requests per fixed 60-second window per API key — one bucket across all of /v1 — and answers 429 `rate_limited` with `Retry-After`; admitted responses from those routes carry the IETF `RateLimit-Limit` / `RateLimit-Policy` (and, when known, `RateLimit-Remaining` / `RateLimit-Reset`) headers. The template routes (`POST /templates/{id}/generate`, `POST /templates/{id}/instantiate`, `POST /documents/{id}/confirm`) additionally require the Enterprise template-channels entitlement, and `POST /embedded/sign-sessions` the Enterprise embedded-signing entitlement; both answer 402 `enterprise-required` without it. Every create endpoint (`POST /signing-requests`, `POST /templates/{id}/instantiate`, `POST /templates/{id}/generate`) accepts an optional flat `metadata` object — your own reference — that is stored on the document and echoed on the create response, on `GET /documents/{id}` and on every webhook that names the document. Signer objects (POST /signing-requests `signers[]`) and recipient objects (instantiate and confirm `recipients[]`) are strict: an unknown key answers 400 `unknown_signer_field` / `unknown_recipient_field` naming every offender. An unknown TOP-LEVEL key is ignored and named in the success body's `warnings[]` (code `unknown_field`); from 2026-12-31 it is refused with 400 `unknown_field`.\n\nVERSIONING. `info.version` is the release date of this document. Every release is frozen at https://www.wesign.now/openapi/<info.version>.json and listed, with what changed and what an integrator must do, in the machine-readable changelog https://www.wesign.now/api-changelog.json (policy: https://www.wesign.now/docs/changelog#versioning-policy).",
    "contact": {
      "name": "wesign.now support",
      "url": "https://www.wesign.now/docs/support"
    },
    "x-spec-revision": "2026-10-03"
  },
  "servers": [
    {
      "url": "https://api.wesign.now/v1",
      "description": "canonical"
    },
    {
      "url": "https://api.letssign.now/v1",
      "description": "alias, permanent"
    }
  ],
  "security": [
    {
      "bearer": []
    }
  ],
  "tags": [
    {
      "name": "Auth",
      "description": "Start here: GET /me validates the key and reports the workspace, the key's scopes, `capabilities` and `sms_allowance`."
    },
    {
      "name": "Signing requests",
      "description": "Send a PDF for signature and act on individual signer rows."
    },
    {
      "name": "Documents",
      "description": "Document-level status, sealed PDF, audit trail and the review (staged instance) lifecycle."
    },
    {
      "name": "Templates",
      "description": "Locked templates: discovery, input schema, generation and instantiation. Generate/instantiate are Enterprise-gated. Fill flow: GET /templates/{id} → instantiate with `validate_only: true` → instantiate."
    },
    {
      "name": "Fields",
      "description": "The workspace field registry that names the `field_values` a template accepts."
    },
    {
      "name": "Hooks",
      "description": "Workspace-wide webhook subscriptions over the API: list, subscribe (re-subscribing a target_url replaces its hook), unsubscribe, rotate the signing secret, read a hook's delivery log, redeliver a delivery and send a test ping."
    },
    {
      "name": "Embedded",
      "description": "Embedded signing: your signer signs inside your own page. Mint a short-lived, single-signer session and render its embed_url in an iframe or webview. (The older guest-placement embedded sessions are retired; those rows can still be read and revoked.)"
    },
    {
      "name": "Retired",
      "description": "Endpoints that answer a stable 410 so integrations fail loud instead of 404."
    }
  ],
  "x-webhook-signature": {
    "canonical_headers": [
      "X-WeSign-Signature",
      "X-WeSign-Event",
      "X-WeSign-Event-Id"
    ],
    "legacy_headers": [
      "X-LetsSign-Signature",
      "X-LetsSign-Event",
      "X-LetsSign-Event-Id"
    ],
    "note": "Both families are sent on every delivery with byte-identical values. The X-LetsSign-* names predate the wesign.now rename and are frozen forever; a receiver may verify either.",
    "signature_format": "t=<unix seconds>,v1=<64 lowercase hex chars>[,v1=<64 lowercase hex chars>]",
    "algorithm": "HMAC-SHA256",
    "message": "${t}.${rawBody}",
    "raw_body": "The exact JSON string POSTed, byte for byte — verify against the raw request body, not a re-serialised object.",
    "verify": "For EACH `v1=` entry: v1 === hex(hmac_sha256(secret, `${t}.${rawBody}`)), compared with a constant-time comparison (e.g. crypto.timingSafeEqual). Accept the delivery when ANY entry matches. Normally there is exactly one entry; for 24 hours after POST /hooks/{id}/rotate there are two (new secret first, then the previous one), so a receiver that only reads the first entry must switch to the new secret immediately after rotating.",
    "rotation": {
      "endpoint": "POST /hooks/{id}/rotate",
      "overlap_seconds": 86400,
      "header_during_overlap": "t=<t>,v1=<hmac with new secret>,v1=<hmac with previous secret>",
      "note": "The previous secret keeps verifying until `previous_secret_valid_until` from the rotate response. Rotating again inside the window replaces the previous secret, so the older one stops verifying at once — at most two secrets are ever live. Both header families carry the identical multi-entry value."
    },
    "secret_format": "whsec_<48 hex> — the HMAC key is this whole string, prefix included, as UTF-8 bytes.",
    "secret_source": "Shown exactly once, at creation: as `secret` in the POST /hooks response, as `callback.secret` in the POST /signing-requests response when `callback_url` is set, or in the dashboard when a workspace webhook is created in Settings → API. No call returns it again (GET /hooks lists hooks without it); to get a new one, POST /hooks/{id}/rotate — the old secret stays valid for 24 hours so you can switch without a gap.",
    "user_agent": "letssign.now-webhooks/1.0",
    "content_type": "application/json",
    "timeout_seconds": 10,
    "success": "Any 2xx status. Anything else, no response within 10 s, or a connection error is a failure and is retried. Redirects are not followed: a 3xx is a failure, and the delivery log records its Location.",
    "retry_schedule": [
      "1m",
      "5m",
      "30m",
      "2h",
      "6h",
      "12h",
      "24h"
    ],
    "max_attempts": 8,
    "user_agent_by_payload": {
      "full": "letssign.now-webhooks/1.0",
      "minimal": "wesign-webhooks/1"
    },
    "user_agent_note": "`user_agent` is the full hooks' User-Agent, frozen; a hook with `payload: \"minimal\"` sends `wesign-webhooks/1`.",
    "retry_note": "The delay before each retry, counted from the attempt before it; after the last one the delivery gives up (about 45 h from the first attempt). Retries are fired by a scheduler that runs every 5 minutes, so a step can fire up to 5 minutes after its due time. Every attempt carries the same event_id and a fresh `t` and signature. A delivery that gave up can be sent again with POST /hooks/{id}/deliveries/{delivery_id}/redeliver."
  },
  "paths": {
    "/me": {
      "get": {
        "tags": [
          "Auth"
        ],
        "operationId": "getMe",
        "summary": "Validate the API key and return its workspace, capabilities and SMS allowance",
        "description": "Connectors call this as their authentication test; it doubles as a human-readable \"who am I\". Also the feature-detection call: `capabilities` says what the workspace may do on the API (embedded signing, templates, …) as booleans — branch on those, never on `workspace.tier`. Deliberately exempt from the per-key rate limit — it never consumes a token and carries no RateLimit-* headers. `sms_allowance` is this month's SMS allowance (`monthly`, `used`, `remaining`, `resets_at`).",
        "responses": {
          "200": {
            "description": "The key is valid. The example is an Enterprise workspace with 3 paid seats (SMS allowance 3 × 100).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Me"
                },
                "example": {
                  "workspace": {
                    "id": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
                    "name": "Muster Treuhand AG",
                    "slug": "mustertreuhand",
                    "tier": "enterprise"
                  },
                  "key": {
                    "id": "0b9a8c7d-6e5f-4a3b-9c2d-1e0f9a8b7c6d",
                    "scopes": [
                      "full"
                    ]
                  },
                  "capabilities": {
                    "embedded_signing": true,
                    "templates": true,
                    "webhooks": true,
                    "sms_verification": true,
                    "ip_allowlist": true,
                    "metadata": true
                  },
                  "sms_allowance": {
                    "monthly": 300,
                    "used": 42,
                    "remaining": 258,
                    "resets_at": "2026-10-01T00:00:00.000Z"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          }
        }
      }
    },
    "/fields": {
      "get": {
        "tags": [
          "Fields"
        ],
        "operationId": "listFields",
        "summary": "List the workspace's field / API-object registry",
        "description": "Read-only discovery of the `field_values` a template can take: each definition's key, kind, type, enum options, example and max length. Curated in the app under Settings → API.",
        "responses": {
          "200": {
            "description": "The registry (empty array when the workspace has none).",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "fields"
                  ],
                  "properties": {
                    "fields": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FieldDefinition"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/hooks": {
      "get": {
        "tags": [
          "Hooks"
        ],
        "operationId": "listHooks",
        "summary": "List the workspace's REST-hook subscriptions",
        "description": "Workspace-wide hooks only (document-scoped hooks created from `callback_url` are not listed), newest first, never with the secret. ACTIVE hooks only by default: a hook missing from this list is inactive — deleted, switched off in the dashboard, or replaced by a POST with the same target_url. `include_disabled=true` adds the switched-off ones (`enabled: false`), so a connector's \"is my hook set up?\" check can answer \"it exists but is switched off\" instead of creating a duplicate. There is no GET /hooks/{id}: list and filter by id. We never switch a hook off on our own because its deliveries fail.",
        "parameters": [
          {
            "name": "include_disabled",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            },
            "description": "Pass exactly `true` to include disabled hooks (`enabled: false`). Any other value, or omitting it, lists enabled hooks only — the listing connectors have always seen."
          }
        ],
        "responses": {
          "200": {
            "description": "Subscriptions, newest first.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "hooks"
                  ],
                  "properties": {
                    "hooks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Hook"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`db_failed` — the read failed. Retry; never read it as \"no hooks\".",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Hooks"
        ],
        "operationId": "createHook",
        "summary": "Subscribe a target URL to workspace events",
        "description": "Creates a workspace-wide webhook and returns its signing secret ONCE, in this response only. Store `secret` before discarding the response: GET /hooks never includes it and nothing reads it back; lose it and you rotate it with POST /hooks/{id}/rotate (the old secret stays valid 24 h) or re-POST. Deliveries are signed with it (see the top-level `x-webhook-signature` and the `webhooks` section).\n\nRe-subscribing REPLACES: when an ACTIVE workspace-wide hook of this workspace already targets the same URL, it is switched off (it drops out of GET /hooks) and this call returns the new hook — a new id and a new secret — naming the old one in `replaced_hook_ids`. Deliveries of the old hook still being retried, for events the new one subscribes to, move to the new hook with their `event_id`. That is how to change the events or the payload mode of a hook. Left out, `events` means every event, while `payload` keeps a `minimal` hook minimal. Redeliver the old hook's given-up deliveries before re-POSTing: a replaced hook's log stays readable, but it cannot redeliver. Two registrations racing leave the newest active.\n\nAnswers 200 (not 201).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HookCreate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Subscribed. The hook plus `secret`, shown once — it is not in any later response.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HookCreated"
                }
              }
            }
          },
          "400": {
            "description": "One of: `invalid_request` — the body is not valid JSON, `target_url` is missing, not a URL or longer than 2048, `events` is not an array of strings or names more than 10 events, `payload` is neither `full` nor `minimal` (`error` says which); `https_required` — `target_url` is not https; `url_not_public` — `target_url` is or resolves to a private, loopback, link-local or reserved address; `unknown_event` — `events` names an event we do not know: `meta.unknown` lists them, `meta.valid_events` every valid name. Nothing is created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`db_failed` — the insert failed, or switching off the hook it replaces failed (then nothing changed). Retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`payload_mode_unavailable` — `payload: \"minimal\"` on a deployment without support for minimal payloads; production supports them. Nothing was created; retry later or omit `payload`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/hooks/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "delete": {
        "tags": [
          "Hooks"
        ],
        "operationId": "deleteHook",
        "summary": "Unsubscribe a REST hook",
        "description": "Deletes the workspace-wide hook and its delivery log. 204 with no body. Idempotent: an unknown or already-deleted id, another workspace's hook and a document-scoped `callback_url` hook all answer 204 and nothing is deleted.",
        "responses": {
          "204": {
            "description": "Deleted, or nothing to delete. No body.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`db_failed` — the delete failed. Retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/hooks/{id}/rotate": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "post": {
        "tags": [
          "Hooks"
        ],
        "operationId": "rotateHookSecret",
        "summary": "Rotate a REST hook's signing secret with a 24-hour overlap",
        "description": "Mints a new `whsec_` secret and returns it ONCE. The previous secret keeps verifying for 24 hours (`previous_secret_valid_until`): every delivery in that window carries two signature entries, new first — `t=<t>,v1=<hmac with new secret>,v1=<hmac with old secret>` on both X-WeSign-Signature and X-LetsSign-Signature — so a receiver that tries every `v1=` entry (Stripe convention) can switch secrets without a gap. A receiver that only reads the first entry must switch to the new secret immediately. Rotating again inside the window replaces the previous secret: the older one stops verifying immediately. Works on any workspace-wide hook (REST or dashboard-created, enabled or disabled); document-scoped `callback_url` hooks answer 404. No request body.",
        "responses": {
          "200": {
            "description": "Rotated. `secret` is the new plaintext, shown once.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HookSecretRotated"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "description": "`not_found` — no workspace-wide hook with this id in the key's workspace (a hook belonging to another workspace, or a document-scoped `callback_url` hook, answers 404 too).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`rotation_conflict` — another rotate call changed the secret between read and write. Retry; the secret in the other call's response is the live one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`db_failed` — the read or update failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/hooks/{id}/deliveries": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "get": {
        "tags": [
          "Hooks"
        ],
        "operationId": "listHookDeliveries",
        "summary": "List a hook's last 100 deliveries",
        "description": "Newest first, at most 100: per delivery the event, its `event_id` and `document_id`, the attempts so far, what your endpoint last answered (status and the first 500 characters of the body), how long that took, and the state. The body we sent is not included. Disabled hooks keep their log (a hook replaced by a re-POST included); a deleted hook's log is gone with it. The same log is in the dashboard under Developers → Webhooks.",
        "responses": {
          "200": {
            "description": "The log, newest first.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "deliveries"
                  ],
                  "properties": {
                    "deliveries": {
                      "type": "array",
                      "maxItems": 100,
                      "items": {
                        "$ref": "#/components/schemas/HookDelivery"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "description": "`not_found` — no workspace-wide hook with this id in the key's workspace (another workspace's hook and a document-scoped `callback_url` hook answer 404 too).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`db_failed` — a database read or write failed. Retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/hooks/{id}/deliveries/{delivery_id}/redeliver": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        },
        {
          "name": "delivery_id",
          "in": "path",
          "required": true,
          "description": "A delivery `id` from GET /hooks/{id}/deliveries.",
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "tags": [
          "Hooks"
        ],
        "operationId": "redeliverHookDelivery",
        "summary": "Redeliver one delivery",
        "description": "Sends the delivery again — typically one that gave up while your endpoint was down. It is the SAME delivery: the same `event_id` (your idempotency check holds) and the same body, in the hook's current payload mode, signed afresh (a new `t`, the hook's current secret). One attempt is made at once and its outcome returned; if it fails, the delivery carries on along the retry schedule from its attempt count, or gives up again if it has none left. A delivered one may be redelivered too. No request body. Only for an https hook (a hook registered over http before 2026-10-02 answers 409 `https_required`), and at most 10 test pings and redeliveries per hook per minute, together (then 429 `rate_limited` with `Retry-After`).",
        "responses": {
          "200": {
            "description": "The attempt was made; `status` says how it went.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HookRedelivered"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "description": "`not_found` — no workspace-wide hook with this id in the key's workspace, or no delivery with this id on that hook.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`hook_disabled` — the hook is switched off (or was replaced by a POST with the same target_url); a delivery to it would be given up at once. Re-enable it first. A replaced hook's deliveries cannot be redelivered on the replacing hook either: redeliver given-up deliveries before re-POSTing the target_url. `https_required` — the hook's URL is http (registered before 2026-10-02): it still receives its events, but test pings and redeliveries go to https hooks only. Register the endpoint again with an https URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — either the API key's bucket (60 requests a minute across /v1, see RateLimited) or this hook's own limit of 10 test pings and redeliveries a minute, together. Retry after `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_failed` — a database read or write failed. Retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/hooks/{id}/test": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "post": {
        "tags": [
          "Hooks"
        ],
        "operationId": "testHook",
        "summary": "Send the hook a signed ping",
        "description": "Delivers `{\"event\":\"ping\",\"event_id\":\"evt_…\",\"created_at\":\"…\"}` (PingEvent) to this hook only, whatever it subscribes to, with the usual headers and signature, and returns the outcome of that one attempt — it is never retried. Use it to check that your endpoint is reachable and verifies our signature. It shows in the delivery log as event `ping`. No request body. Only for an https hook (a hook registered over http before 2026-10-02 answers 409 `https_required`), and at most 10 test pings and redeliveries per hook per minute, together (then 429 `rate_limited` with `Retry-After`).",
        "responses": {
          "200": {
            "description": "The ping was attempted; `status` says how it went.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HookTestResult"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "description": "`not_found` — no workspace-wide hook with this id in the key's workspace (another workspace's hook and a document-scoped `callback_url` hook answer 404 too).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`hook_disabled` — the hook is switched off (or was replaced by a POST with the same target_url); it receives nothing, a test included. Re-enable it first. `https_required` — the hook's URL is http (registered before 2026-10-02): it still receives its events, but test pings and redeliveries go to https hooks only. Register the endpoint again with an https URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — either the API key's bucket (60 requests a minute across /v1, see RateLimited) or this hook's own limit of 10 test pings and redeliveries a minute, together. Retry after `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_failed` — a database read or write failed. Retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/signing-requests": {
      "post": {
        "tags": [
          "Signing requests"
        ],
        "operationId": "createSigningRequest",
        "summary": "Send a PDF for signature in one call",
        "description": "Uploads (or fetches) a PDF, places fields, creates one signing request per signer, emails the pending signers (unless `send_emails: false`) and returns every signing URL with the per-signer read-back (SignerReadback). Send the PDF either as `multipart/form-data` (`file` = the bytes; every other field a JSON-encoded string) or as `application/json` with `file_url` (fetched server-side, SSRF-guarded, no redirects). Max 25 MB, `application/pdf` only.\n\nPlacement: `anchors` (default) scans `[[ls:KIND:ROLE]]` markers and masks them — with zero matches it falls through to `auto_append`; `auto_append` appends a signature page with one block per signer; `explicit` uses `fields[]` (normalised page-fraction coordinates); `manual` is retired and answers 400 `placement_retired` before anything is created. The optional third part of a `text` or `name` marker (`[[ls:text:ROLE:FIELD_KEY]]`) is stored as the box's field key: a key that asks for the signer's own details (`person.full_name`, `person.first_name`, `person.last_name`, `contact.email`, `company.legal_name`, or the short `full_name`, `first_name`, `last_name`, `email`, `company`, `role`) opens the box with them, and the signer can change it. A `name` marker without a third part is keyed `person.full_name`. On a `date` marker the third part is not a key; this endpoint ignores it.\n\n`metadata` (optional, flat object — see the ApiMetadata schema) is your own reference: stored on the document, echoed as `metadata` in the 201, on GET /documents/{id} and on every webhook for the document. Malformed → 400 `invalid_metadata`, refused before any upload or insert.\n\n`callback_url` registers a document-scoped webhook; its HMAC secret is returned ONCE under `callback.secret`.\n\nSupports `Idempotency-Key`: same key + same body replays the original 201 with `Idempotent-Replayed: true`; same key + different body answers 422 `idempotency_key_reuse`; a concurrent retry answers 409 `idempotency_in_progress` with `Retry-After`.\n\nSigner objects are strict (400 `unknown_signer_field`). A signer may carry the `company` it signs for and its `job_title`; both print under the signature and are read back. An unknown top-level key is ignored and named in `warnings[]` (400 `unknown_field` from 2026-12-31).\n\nAll or nothing: the document, its callback webhook, every signing request and every field are written before anything is sent. Only then do the invitations go out, followed by the observer notice and the one `signing_request.sent`. If a write fails, everything this call created is deleted again, its uploaded PDF included, nobody is contacted, and the call answers 500 `db_failed` with `meta.rolled_back`; the `Idempotency-Key` is released, so a retry starts from nothing.\n\nSMS: `require_sms_verification` + `phone_e164` per signer, `sms_gate` for when the code is asked. When the signers who must verify by SMS outnumber the codes left in the workspace's monthly allowance, the call answers 402 `sms_allowance_exhausted` before anything is uploaded or created.",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SigningRequestCreate"
                  },
                  {
                    "required": [
                      "file_url"
                    ]
                  }
                ]
              }
            },
            "multipart/form-data": {
              "schema": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SigningRequestCreate"
                  },
                  {
                    "properties": {
                      "file": {
                        "type": "string",
                        "contentMediaType": "application/pdf",
                        "description": "The PDF bytes (≤ 25 MB). Either `file` or `file_url` is required."
                      }
                    }
                  }
                ]
              },
              "encoding": {
                "signers": {
                  "contentType": "application/json"
                },
                "fields": {
                  "contentType": "application/json"
                },
                "observer_emails": {
                  "contentType": "application/json"
                },
                "metadata": {
                  "contentType": "application/json"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created. Also returned on an `Idempotency-Key` replay (with `Idempotent-Replayed: true`) — including `callback.secret`, which is part of the cached body.",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/Idempotent-Replayed"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SigningRequestCreated"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` (body/JSON/zod, or neither `file` nor `file_url`, or a duplicate `signing_order` in sequential mode; includes `phone_e164 is required when require_sms_verification is true`, a `company` or `job_title` that is too long or holds a line break or other control character, `sms_gate \"before_view\" requires require_sms_verification: true` and a `filename` over 200 characters), `unknown_signer_field` (a signer carries a key the Signer schema does not list — e.g. `phone` or `title`; `meta.signers` = [{ index, fields }] for every offender, `meta.accepted` = the accepted keys), `placement_retired` (placement=\"manual\"), `unknown_role` (an explicit field or an anchor names a role with no signer; `meta.role`), `signer_has_no_anchor` (`meta.role`), `duplicate_anchor` (anchor scan), `invalid_idempotency_key`, `blocked_url` / `redirect_not_allowed` (file_url guard), `invalid_metadata` (`metadata` is not a flat object of string/number/boolean/null values, has a key outside ^[A-Za-z0-9_.-]{1,40}$, more than 16 keys, a string over 500 characters, or exceeds 4096 bytes as JSON; the body carries a top-level `problems` array of `{ path, message }` — refused before the Idempotency-Key lock and before any upload, so no document exists after this answer).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "$ref": "#/components/schemas/MetadataInvalid"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "`tier_required` — the workspace's monthly document cap is reached (`meta: { tier, cap, used }`); or `sms_allowance_exhausted` — the signers who must verify by SMS outnumber the SMS codes left this month (top-level `sms_allowance` + `required`; checked after the document cap, before any upload or write).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "$ref": "#/components/schemas/SmsAllowanceExhausted"
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "description": "`workspace_not_found` — the key's workspace row is gone.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_in_progress` — an earlier call with the same `Idempotency-Key` is still running; retry after `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`file_too_large` — the PDF exceeds 25 MB (`meta.size` for uploads).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "`unsupported_media_type` — the body is neither multipart nor JSON, the multipart parse failed, or the file is not `application/pdf`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`idempotency_key_reuse` (same key, different body), `signer_without_signature` (explicit placement leaves a signer with no signature/initial field; `meta.roles`), `placement_failed` (PDF mutation failed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`storage_failed` (the PDF upload failed; nothing was created) or `db_failed` (a write failed). On `db_failed` nothing was sent: `meta.rolled_back: true` means every row this call wrote and its PDF were deleted, and `error` ends \"Nothing was created or sent; retry the request.\"; `meta.rolled_back: false` (rare) means the partly created document could not be deleted: `meta.document_id` names it, its open signing requests are withdrawn where possible, and we are alerted. `meta.role` names the signer whose signing request or fields failed. A failed callback webhook insert and a failed field insert answer this too (before, the first was ignored and the second left the signer with nothing to sign). The `Idempotency-Key` is released, so retrying the same body is safe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`fetch_failed` — `file_url` could not be fetched, returned a non-2xx status or an empty body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "`fetch_timeout` — fetching `file_url` timed out.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/signing-requests/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "get": {
        "tags": [
          "Signing requests"
        ],
        "operationId": "getSigningRequest",
        "summary": "Read one signer's row",
        "description": "Status, signing URL, the signer's SMS factor and reference (SignerReadback: `require_sms_verification`, `phone_masked`, `sms_verified_at`, `sms_gate`, `signature_level`, `reference`) and the FIRST 50 audit events (oldest first) of one signing request. For the whole document (every signer) use GET /documents/{id}. The signing URL contains the signer's credential — treat it as secret. A signer of a staged template instance (`review: true`, not confirmed yet) reads `staged: true` and `signingUrl: null`.",
        "responses": {
          "200": {
            "description": "The signing request.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SigningRequest"
                },
                "example": {
                  "id": "5b2e8f41-7c3a-4d19-9e60-2a8b1c4d7e93",
                  "documentId": "c4a1d9e2-3b7f-4e85-a0c6-9d2e1f3b5a78",
                  "signer": {
                    "email": "anna.muster@example.ch",
                    "name": "Anna Muster",
                    "role": "client",
                    "order": null,
                    "require_sms_verification": true,
                    "phone_masked": "•••• •••• 4567",
                    "sms_verified_at": "2026-10-02T08:14:37.120Z",
                    "sms_gate": "before_view",
                    "signature_level": "AES",
                    "reference": "person-8812",
                    "company": null,
                    "job_title": null
                  },
                  "status": "viewed",
                  "locale": "de",
                  "channel": "email",
                  "expiresAt": "2026-10-16T07:58:02.410Z",
                  "createdAt": "2026-10-02T07:58:02.410Z",
                  "staged": false,
                  "signingUrl": "https://mustertreuhand.wesign.now/de/sign/9f3c0b7e5a2d4c18b6e1f07a3d5c9e24",
                  "auditEvents": [
                    {
                      "type": "email_sent",
                      "createdAt": "2026-10-02T07:58:03.002Z",
                      "meta": {
                        "to": "anna.muster@example.ch",
                        "source": "v1_api",
                        "api_key_id": "0b9a8c7d-6e5f-4a3b-9c2d-1e0f9a8b7c6d"
                      }
                    },
                    {
                      "type": "sms_verify_sent",
                      "createdAt": "2026-10-02T08:13:51.884Z",
                      "meta": {
                        "masked_phone": "•••• •••• 4567",
                        "quota_used": 43,
                        "quota_cap": 300
                      }
                    },
                    {
                      "type": "sms_verify_ok",
                      "createdAt": "2026-10-02T08:14:37.120Z",
                      "meta": {}
                    },
                    {
                      "type": "viewed",
                      "createdAt": "2026-10-02T08:14:39.506Z",
                      "meta": null
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "tags": [
          "Signing requests"
        ],
        "operationId": "updateSigningRequestPhone",
        "summary": "Correct the phone a signer receives the SMS code on",
        "description": "Changes `phone` and nothing else: `require_sms_verification`, `sms_gate`, the link and the status stay, and nothing is sent — no invitation, no code. The signer requests the code on the sign page, which now goes to the new number; a code already sent to the old number cannot verify the new one. Allowed while the request is `pending`, `viewed` or `queued` AND the signer has not verified a code yet; otherwise 409 `not_editable` (once verified, the number is part of the evidence). The write is a compare-and-set on exactly those conditions. The same number again answers 200 without a write. A change writes the audit event `phone_changed` with both numbers masked. Strict body: `phone_e164` only. Same auth, workspace scope and rate limit as GET; answers the GET representation. To add or remove the SMS factor, or to reach a signer on another number after they verified, withdraw the request and create a new one.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "phone_e164"
                ],
                "properties": {
                  "phone_e164": {
                    "type": "string",
                    "pattern": "^\\+[1-9]\\d{7,14}$",
                    "description": "The new mobile number, E.164 (trimmed). null is not accepted: removing the phone of an SMS signer would lock them out."
                  }
                }
              },
              "example": {
                "phone_e164": "+41791234567"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The signing request after the change (the GET shape).",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SigningRequest"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` (body is not JSON, not an object, or `phone_e164` missing / not a string), `unknown_field` (a key other than `phone_e164`; `meta.fields`, `meta.accepted`), `invalid_phone` (not E.164).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`not_editable` — the request is not `pending` / `viewed` / `queued`, or the signer already verified a code (`meta.status`, `meta.sms_verified_at`). To reach a signer on another number after that, withdraw and create a new request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`db_failed` — the update failed; nothing changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/signing-requests/{id}/remind": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "post": {
        "tags": [
          "Signing requests"
        ],
        "operationId": "remindSigningRequest",
        "summary": "Re-send the signing email",
        "description": "Only `pending` / `viewed`, unexpired requests can be reminded. A per-signer cooldown refuses a second reminder within 60 seconds of the last one, whoever sent it (429 `rate_limited` with `Retry-After: 60`, distinct from the per-key window).",
        "responses": {
          "200": {
            "description": "Reminder sent.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`invalid_state` (request is not pending/viewed) or `expired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — either the per-key window (with `Retry-After` and `RateLimit-*`) or the 60-second per-signer reminder cooldown (`Retry-After: 60`, no `RateLimit-*` headers).",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`email_failed` — the email could not be sent: the mail provider rejected it, or the signer's address is on a domain that accepts no email (a null MX, no such domain, or no mail server; nothing is sent and `error` names the domain). Retrying does not help with the latter: correct the address (withdraw and create the request again) or deliver the signing link yourself.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`email_not_configured` — outbound email is not configured in this environment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/signing-requests/{id}/withdraw": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "post": {
        "tags": [
          "Signing requests"
        ],
        "operationId": "withdrawSigningRequest",
        "summary": "Cancel a still-in-flight signing request",
        "description": "Flips a `pending` / `viewed` request to `withdrawn`, records the API key in the audit trail and emits `signing_request.withdrawn` (`withdrawn_by: api`) before answering. A `queued` sequential follower cannot be withdrawn here (409 `invalid_state`).",
        "responses": {
          "200": {
            "description": "Withdrawn.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`invalid_state` — the request is not pending/viewed (already signed, expired, withdrawn, queued…).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`db_failed` — the status update failed; nothing changed, the request is still in flight and no webhook fired. Retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/documents": {
      "post": {
        "tags": [
          "Retired"
        ],
        "operationId": "createDocumentRetired",
        "summary": "Retired — manual placement",
        "description": "Retired 2026-07-31 together with the manual-placement flow. Answers a stable 410 without authenticating. Use POST /signing-requests with `file_url` and placement `anchors`, `explicit` or `auto_append`.",
        "deprecated": true,
        "security": [],
        "responses": {
          "410": {
            "description": "`placement_retired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/documents/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "get": {
        "tags": [
          "Documents"
        ],
        "operationId": "getDocument",
        "summary": "Document status with every signer",
        "description": "The natural object for a \"have they signed yet?\" poll: document basics, one entry per signing request, and an aggregate `status`. While the document is a STAGED template instance (nothing sent yet) a `review` block is present, every signer reports `staged: true` and `signingUrl` is null.",
        "responses": {
          "200": {
            "description": "The document.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Document"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/documents/{id}/audit-trail": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "get": {
        "tags": [
          "Documents"
        ],
        "operationId": "getDocumentAuditTrail",
        "summary": "Generate and stream the audit-trail PDF",
        "description": "Built from every signer's audit events; the last signer's row carries the chain's certificate serial and TSA timestamp. Always rendered in English, times in UTC, with each signer's stated function and company (`job_title`, `company`) under their signature. The certificate attached to the completion emails shows the same and is English as well; it prints times in the time zone of the caption under each signature. This is the URL the `document.completed` webhook carries as `audit_trail_url`. Serves under the same rule as `/signed`: every signer row `signed`, else 409 `not_complete`. Unlike `/signed` it never answers 410: it is built from our records, not from the PDF, so it stays available after the workspace's retention period ends and `/signed` answers 410. Rendered on each request, so its bytes can differ between fetches and `sha256` on `document.completed` does not cover it. The SHA-256 it prints is that of the file as sent for signature (`sha256` of GET /documents/{id}), not of the signed PDF; the certificate attached to the completion emails prints the signed PDF's hash instead.",
        "responses": {
          "200": {
            "description": "The audit trail. `Content-Disposition: inline; filename=\"<document filename>-audit-trail.pdf\"` (the same sanitised name as `/signed`, with `-audit-trail.pdf` appended); `Cache-Control: private, no-store`.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "application/pdf"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "description": "`not_found` (unknown id, another workspace, or a document deleted in the app) or `no_signers` (the document has no signing requests). Final: do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`not_complete` (a signer row is not `signed` — still pending, or declined, withdrawn or expired) or `not_stored` (every signer is `signed` but no seal record is there yet — a passing state on our side; retry with backoff).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`render_failed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`temporarily_unavailable` — a database read failed on our side, so nothing can be said about the document yet. Never final: retry after `Retry-After` seconds (5), then with backoff. (Before the 2026-10-10 release the same failure answered 404 `not_found` / `no_signers` or 409 `not_stored`, and the audit trail could be rendered without its events.)",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "A database read failed on our side. Try again in a moment.",
                  "code": "temporarily_unavailable"
                }
              }
            }
          }
        }
      }
    },
    "/documents/{id}/pdf": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "get": {
        "tags": [
          "Documents"
        ],
        "operationId": "getDocumentPdf",
        "summary": "Stream the document's current (un-signed) file",
        "description": "The bytes the document row currently points at. This is how you fetch a finalized FILE-ONLY instance (generated with `review: true` and confirmed) — such a document has no signers, so `/signed` would answer 404 `no_signers`. For an e-sign document this is deliberately the pre-signature original; the sealed PDF is served by `/signed`.",
        "responses": {
          "200": {
            "description": "The file (`Content-Disposition: attachment`).",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "application/pdf"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`not_rendered` — the document has no stored file.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`storage_failed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/documents/{id}/signed": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "get": {
        "tags": [
          "Documents"
        ],
        "operationId": "getDocumentSigned",
        "summary": "Stream the fully-signed, PAdES-sealed PDF",
        "description": "The document sealed by the last signer: one PAdES seal over the whole file, which carries every signer's signature — also when parallel signers sign within the same seconds, because the seals of one document take turns. Before the 2026-09-24 release a multi-signer file kept each earlier seal, which validators then reported as invalid next to the valid last one. This is the URL the `document.completed` webhook carries as `signed_pdf_url` — it requires the owning workspace's Bearer key. Serves only when every signer row is `signed` — exactly the condition under which `document.completed` fires and GET /documents/{id} reports `status: signed`; a still-pending, declined, withdrawn or expired signer answers 409 `not_complete`. Fetch it when `document.completed` arrives and keep your own copy: when the owning workspace has \"Delete signed documents after N days\" on (Settings → Signing → Auto-delete in the dashboard; the API exposes neither the setting nor a deletion date), this endpoint answers 410 `deleted_by_retention` from the first nightly deletion run (03:20 UTC) after N days have passed since the document's last signature. The response carries no ETag or digest header; the only checksum is `sha256` on `document.completed` (and, in the full body, on the last signer's `signing_request.signed`). Retry `not_stored`, 429, 500, 503 `temporarily_unavailable` (after `Retry-After`) and timeouts with backoff; 404 and 410 are final. Guide: https://www.wesign.now/docs/documents#retrieve-the-signed-document.",
        "responses": {
          "200": {
            "description": "The sealed PDF. `Content-Disposition: inline; filename=\"<document filename>\"`, every character other than A–Z, a–z, 0–9, `.`, `_` and `-` replaced by `_`; `Cache-Control: private, no-store`. The whole file in one response; allow 60 s.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "application/pdf"
                }
              }
            }
          },
          "307": {
            "description": "Only a PDF stored before regional storage existed (April 2026) redirects to its storage URL instead of streaming; no document sealed since then answers it. Follow `Location` without sending your API key to that host.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "description": "`not_found` (unknown id, another workspace, or a document deleted in the app) or `no_signers` (the document has no signing requests — use `/pdf` for file-only instances). Final: do not retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`not_complete` (a signer row is not `signed` — still pending, or declined, withdrawn or expired; never follows a genuine `document.completed`) or `not_stored` (every signer is `signed` but the sealed PDF's record is not there yet — it is stored before the last signer's status turns `signed`, so this is a passing state on our side; retry with backoff).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "The workspace's retention setting (\"Delete signed documents after N days\") has taken effect: the first nightly deletion run after N days since the last signature deleted the signed PDF. Final: it cannot be fetched again. GET /documents/{id} (still `status: signed`) and GET /documents/{id}/audit-trail keep answering. `code`: `deleted_by_retention`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "The signed PDF was deleted after the workspace retention period.",
                  "code": "deleted_by_retention"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`storage_failed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`temporarily_unavailable` — a database read failed on our side, so nothing can be said about the document yet. Never final: retry after `Retry-After` seconds (5), then with backoff. (Before the 2026-10-10 release the same failure answered 404 `not_found` / `no_signers` or 409 `not_stored`.)",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "A database read failed on our side. Try again in a moment.",
                  "code": "temporarily_unavailable"
                }
              }
            }
          }
        }
      }
    },
    "/documents/{id}/confirm": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "post": {
        "tags": [
          "Documents"
        ],
        "operationId": "confirmDocument",
        "summary": "Confirm a staged template instance",
        "description": "Bearer-key twin of the dashboard's review confirm. An instance staged with `review: true` (by `/templates/{id}/instantiate` or `/templates/{id}/generate`) is promoted here once a human has looked at it: e-sign instances are dispatched (rows go `pending`, invites go out unless `send_emails` was/is false); file-only instances are rendered, persisted and finalized. Enterprise-gated (402) because confirming sends. Repeat calls on an already-confirmed instance answer 200 with `already_confirmed: true`.\n\nRecipient edits are strict (400 `unknown_recipient_field`). The phone field is `phone_e164`; `phone` is a deprecated alias accepted until 2026-12-31 (each use adds a `deprecated_field` warning; both with different values → 400 `conflicting_fields`). A malformed JSON body answers 400 — an EMPTY body still means \"confirm as staged\". Every check (recipients, pins, values, SMS allowance) runs before anything is written: a refused confirm (400/402/409/422) changes nothing. One exception: a discard or the review expiry that lands while the confirm renders answers 409 `discarded` / `not_staged` after the edits were saved — onto an instance that can no longer be confirmed, so they have no effect. The 200 carries `warnings[]` and the per-recipient read-back.\n\nValues: confirm takes no `field_values` (see ConfirmBody). A reviewer corrects them on `review_url`; confirm validates them against the staged version and copies the Sender / API values onto the signing fields before anyone is invited.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConfirmBody"
              },
              "examples": {
                "edit_recipient": {
                  "summary": "Correct representative 2's email and give the signers 21 days",
                  "value": {
                    "recipients": [
                      {
                        "signing_request_id": "17a5c3e9-8b2d-4f6a-9c1e-5d3b7a0f2e81",
                        "email": "m.rossi@nordlicht-logistik.de"
                      }
                    ],
                    "expires_in_days": 21
                  }
                },
                "as_staged": {
                  "summary": "Confirm exactly as staged (an empty body works too)",
                  "value": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmed (or already confirmed).",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ConfirmResultEsign"
                    },
                    {
                      "$ref": "#/components/schemas/ConfirmResultFile"
                    }
                  ],
                  "discriminator": {
                    "propertyName": "kind"
                  }
                },
                "examples": {
                  "sent": {
                    "summary": "Sent: representative 1 invited, representative 2 queued",
                    "description": "The staged \"Power of attorney (company)\" instance. `re_rendered` is false: on a PDF template the Sender / API values are not rendered into the file; confirm copied them onto the signing fields each seal prints.",
                    "value": {
                      "ok": true,
                      "kind": "esign",
                      "status": "sent",
                      "already_confirmed": false,
                      "re_rendered": false,
                      "document_id": "c6e1a8f3-2d5b-4b07-9e4a-8f1c3d6b0a29",
                      "expires_at": "2026-10-16T08:30:12.441Z",
                      "recipients": [
                        {
                          "signing_request_id": "d9f2b6a1-4c8e-4e3d-a7b0-2e5f8c1d9a64",
                          "slot": 1,
                          "email": "anna.keller@nordlicht-logistik.de",
                          "status": "pending",
                          "signing_url": "https://mustertreuhand.wesign.now/de/sign/4f8a1c6e3b9d2f7a0c5e8b1d4a7f3c92",
                          "emailed": true,
                          "sms_sent": false,
                          "require_sms_verification": true,
                          "phone_masked": "•••• •••• 4567",
                          "sms_verified_at": null,
                          "sms_gate": "before_sign",
                          "signature_level": "AES",
                          "reference": "rep-1",
                          "company": "Nordlicht Logistik GmbH",
                          "job_title": "Geschäftsführerin"
                        },
                        {
                          "signing_request_id": "17a5c3e9-8b2d-4f6a-9c1e-5d3b7a0f2e81",
                          "slot": 2,
                          "email": "m.rossi@nordlicht-logistik.de",
                          "status": "queued",
                          "signing_url": "https://mustertreuhand.wesign.now/de/sign/b2d5f8a1c4e7093b6d9f2a5c8e1b4d70",
                          "emailed": false,
                          "sms_sent": false,
                          "require_sms_verification": false,
                          "phone_masked": null,
                          "sms_verified_at": null,
                          "sms_gate": "before_sign",
                          "signature_level": "SES",
                          "reference": null,
                          "company": "Nordlicht Logistik GmbH",
                          "job_title": "Prokurist"
                        }
                      ],
                      "warnings": []
                    }
                  },
                  "field_values_ignored": {
                    "summary": "A `field_values` key was sent: ignored and named in `warnings`",
                    "description": "Confirm takes no `field_values`. A reviewer corrects values on `review_url` before confirming.",
                    "value": {
                      "ok": true,
                      "kind": "esign",
                      "status": "sent",
                      "already_confirmed": false,
                      "re_rendered": false,
                      "document_id": "c6e1a8f3-2d5b-4b07-9e4a-8f1c3d6b0a29",
                      "expires_at": "2026-10-16T08:30:12.441Z",
                      "recipients": [
                        {
                          "signing_request_id": "d9f2b6a1-4c8e-4e3d-a7b0-2e5f8c1d9a64",
                          "slot": 1,
                          "email": "anna.keller@nordlicht-logistik.de",
                          "status": "pending",
                          "signing_url": "https://mustertreuhand.wesign.now/de/sign/4f8a1c6e3b9d2f7a0c5e8b1d4a7f3c92",
                          "emailed": true,
                          "sms_sent": false,
                          "require_sms_verification": true,
                          "phone_masked": "•••• •••• 4567",
                          "sms_verified_at": null,
                          "sms_gate": "before_sign",
                          "signature_level": "AES",
                          "reference": "rep-1",
                          "company": "Nordlicht Logistik GmbH",
                          "job_title": "Geschäftsführerin"
                        },
                        {
                          "signing_request_id": "17a5c3e9-8b2d-4f6a-9c1e-5d3b7a0f2e81",
                          "slot": 2,
                          "email": "m.rossi@nordlicht-logistik.de",
                          "status": "queued",
                          "signing_url": "https://mustertreuhand.wesign.now/de/sign/b2d5f8a1c4e7093b6d9f2a5c8e1b4d70",
                          "emailed": false,
                          "sms_sent": false,
                          "require_sms_verification": false,
                          "phone_masked": null,
                          "sms_verified_at": null,
                          "sms_gate": "before_sign",
                          "signature_level": "SES",
                          "reference": null,
                          "company": "Nordlicht Logistik GmbH",
                          "job_title": "Prokurist"
                        }
                      ],
                      "warnings": [
                        {
                          "field": "field_values",
                          "code": "unknown_field",
                          "message": "\"field_values\" is not a parameter of this endpoint and was ignored. From 2026-12-31 an unknown top-level key is refused with 400 unknown_field."
                        }
                      ]
                    }
                  },
                  "already_confirmed": {
                    "summary": "A repeat call",
                    "value": {
                      "ok": true,
                      "kind": "esign",
                      "status": "sent",
                      "already_confirmed": true,
                      "re_rendered": false,
                      "document_id": "c6e1a8f3-2d5b-4b07-9e4a-8f1c3d6b0a29",
                      "expires_at": "2026-10-16T08:30:12.441Z",
                      "recipients": [],
                      "warnings": []
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` (body validation — first issue in `error` — or \"Invalid JSON body\"), `unknown_recipient_field` (`meta.recipients` = [{ index, fields }], `meta.accepted`), `conflicting_fields` (`phone_e164` and `phone` both sent with different values; `meta.recipients` = [{ index, fields: [\"phone_e164\",\"phone\"] }]) or `pinned-slot-mismatch` (a recipient override breaks a slot pin; `slot` names it).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConfirmError"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "`enterprise-required` (the workspace lacks template channels) or `sms_allowance_exhausted` (the still-staged recipients who must verify by SMS — waiters released for later included — outnumber the codes left this month; nothing was written).",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/EnterpriseRequiredBody"
                    },
                    {
                      "$ref": "#/components/schemas/SmsAllowanceExhausted"
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`not_staged` (nothing awaiting review), `discarded` (the review was discarded), `interactive-table-requires-sequential`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConfirmError"
                }
              }
            }
          },
          "410": {
            "description": "`review_expired` — the 14-day review window has passed; discard and create a new instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConfirmError"
                }
              }
            }
          },
          "422": {
            "description": "`invalid_recipients` (`problems: [{ signing_request_id, field, message }]`; `field` is signing_request_id (no such staged recipient on this document), email, phone (named `phone` whichever spelling you sent — `phone_e164` or its deprecated alias; the check runs on the stored number), channel, require_sms_verification, sms_gate or signing_order — checked on the row as edited; nothing is written) or `template_input_invalid` (the stored values no longer satisfy the template's input schema; `problems` as on generate/instantiate).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConfirmError"
                },
                "examples": {
                  "template_input_invalid": {
                    "summary": "`template_input_invalid`: a reviewer emptied a required Sender / API value",
                    "description": "Confirm answers the short form: `error`, `code` and `problems` (no `docs`, `warnings` or template fields). Nothing was written or sent.",
                    "value": {
                      "error": "1 problem with this request.",
                      "code": "template_input_invalid",
                      "problems": [
                        {
                          "field": "company.tax_id",
                          "label": "Company tax ID",
                          "code": "field_required",
                          "message": "\"Company tax ID\" is required by this template but was not supplied."
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "`update_failed`, `confirm_failed` or `render_failed` — nothing was sent; retry the confirmation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConfirmError"
                }
              }
            }
          }
        }
      }
    },
    "/documents/{id}/discard": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "post": {
        "tags": [
          "Documents"
        ],
        "operationId": "discardDocument",
        "summary": "Discard a staged template instance",
        "description": "Bearer-key twin of the dashboard's review discard. E-sign instances have their staged rows withdrawn; file-only instances are deleted outright. Deliberately NOT Enterprise-gated so a workspace that lost the entitlement can still clean up. No request body.",
        "responses": {
          "200": {
            "description": "Discarded (or already discarded).",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DiscardResult"
                },
                "examples": {
                  "discarded": {
                    "summary": "Discarded: both staged signing requests withdrawn",
                    "value": {
                      "ok": true,
                      "status": "discarded",
                      "kind": "esign",
                      "document_id": "c6e1a8f3-2d5b-4b07-9e4a-8f1c3d6b0a29",
                      "already_discarded": false,
                      "withdrawn": 2,
                      "deleted": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`already_confirmed` (the review was confirmed, nothing to discard) or `not_staged`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "502": {
            "description": "`discard_failed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/templates": {
      "get": {
        "tags": [
          "Templates"
        ],
        "operationId": "listTemplates",
        "summary": "List the workspace's templates",
        "description": "Every non-archived template, newest update first. Only `instantiable: true` (status `locked`) templates can be generated from or instantiated. Not Enterprise-gated — discovery works on any key.",
        "responses": {
          "200": {
            "description": "The templates.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "templates"
                  ],
                  "properties": {
                    "templates": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Template"
                      }
                    }
                  }
                },
                "examples": {
                  "templates": {
                    "summary": "Two locked templates and a draft",
                    "description": "Newest update first. Only `instantiable: true` (status `locked`) can be generated from or instantiated; archived templates are never listed.",
                    "value": {
                      "templates": [
                        {
                          "id": "8d3f1a52-6b0e-4c7a-9f21-3e5d7c9b1a04",
                          "name": "Power of attorney (company)",
                          "description": "Company power of attorney signed by two authorised representatives.",
                          "status": "locked",
                          "version": 3,
                          "pages": 2,
                          "content_kind": "pdf",
                          "instantiable": true,
                          "updated_at": "2026-09-24T09:12:44.518Z"
                        },
                        {
                          "id": "5e0b7d93-1c4f-4a26-8b3e-7f2d9a6c0e18",
                          "name": "Service agreement",
                          "description": null,
                          "status": "draft",
                          "version": 1,
                          "pages": 3,
                          "content_kind": "richtext",
                          "instantiable": false,
                          "updated_at": "2026-09-23T11:05:37.902Z"
                        },
                        {
                          "id": "2c7e9b14-5a3d-4f08-b6e1-9d4a0c3f7e25",
                          "name": "Mandate letter",
                          "description": "Engagement letter for a new client mandate.",
                          "status": "locked",
                          "version": 2,
                          "pages": 1,
                          "content_kind": "richtext",
                          "instantiable": true,
                          "updated_at": "2026-09-18T15:40:02.117Z"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/templates/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "get": {
        "tags": [
          "Templates"
        ],
        "operationId": "getTemplate",
        "summary": "One template plus its published input schema",
        "description": "The recipient slots, the `field_values` the template accepts (inline placeholders, fillable text fields and collections, enriched from the field registry and the standard field catalogue) and the signer-drawn signature/initial/date fields. A caller must supply exactly the entries where `required` is true; `owner` says who fills each one — on a PDF template the author picks per text field whether the sender (you) or the signer fills it. `standard`, `labels`, `example` and `pattern` tell your app how to ask its user for each value.\n\nPreparing a fill in your app: read this schema → ask your user for every `required` field (and, as they like, the others) → POST /instantiate with `validate_only: true` → fix `problems`, offer `missing_optional` → send (step by step: https://www.wesign.now/docs/prepare-a-fill). The same schema object is what `/generate` and `/instantiate` validate against. Not Enterprise-gated.",
        "responses": {
          "200": {
            "description": "The template and its input schema.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateDetail"
                },
                "examples": {
                  "pdf_template": {
                    "summary": "PDF template: Sender / API boxes on slot 1, a Signer box on each slot",
                    "description": "\"Power of attorney (company)\" v3, signed by two representatives. `company.legal_name` and `company.tax_id` are Sender / API boxes (`owner: sender`, `required: true`) stored on slot 1: you must send them, and every signer sees them as fixed content. `signing.place` (slot 1) and `signing.place_s2` (slot 2) are Signer boxes: never required of you, you may prefill them. Its slots have no role or pin, so each is just `{ slot }`; a template that has them publishes `role`, `pinned` and `pinned_email` on the slot. The author left the boxes unlabelled, so `label` is the standard English name.",
                    "value": {
                      "template": {
                        "id": "8d3f1a52-6b0e-4c7a-9f21-3e5d7c9b1a04",
                        "name": "Power of attorney (company)",
                        "description": "Company power of attorney signed by two authorised representatives.",
                        "status": "locked",
                        "version": 3,
                        "pages": 2,
                        "content_kind": "pdf",
                        "instantiable": true,
                        "updated_at": "2026-09-24T09:12:44.518Z"
                      },
                      "recipients": [
                        {
                          "slot": 1
                        },
                        {
                          "slot": 2
                        }
                      ],
                      "field_values": [
                        {
                          "key": "company.legal_name",
                          "kind": "scalar",
                          "owner": "sender",
                          "required": true,
                          "signer_required": false,
                          "has_default": false,
                          "slot": 1,
                          "label": "Company name",
                          "type": "text",
                          "source": "lead",
                          "example": "Muster AG",
                          "standard": true,
                          "labels": {
                            "en": "Company name",
                            "de": "Firma",
                            "fr": "Raison sociale",
                            "it": "Ragione sociale",
                            "es": "Razón social",
                            "nl": "Bedrijfsnaam"
                          },
                          "description": "The company's registered legal name, including its legal form (Muster AG)."
                        },
                        {
                          "key": "company.tax_id",
                          "kind": "scalar",
                          "owner": "sender",
                          "required": true,
                          "signer_required": false,
                          "has_default": false,
                          "slot": 1,
                          "label": "Company tax ID",
                          "type": "text",
                          "source": "lead",
                          "example": "DE123456789-00001",
                          "standard": true,
                          "labels": {
                            "en": "Company tax ID",
                            "de": "Steuer-ID des Unternehmens (z. B. W-IdNr.)",
                            "fr": "Numéro fiscal (entreprise)",
                            "it": "Numero fiscale (azienda)",
                            "es": "Número fiscal (empresa)",
                            "nl": "Fiscaal nummer (bedrijf)"
                          },
                          "description": "The company's tax number, e.g. the German W-IdNr DE123456789-00001. The format depends on the country."
                        },
                        {
                          "key": "signing.place",
                          "kind": "scalar",
                          "owner": "signer",
                          "required": false,
                          "signer_required": false,
                          "has_default": false,
                          "slot": 1,
                          "label": "Place of signing",
                          "type": "text",
                          "source": "lead",
                          "example": "Zürich",
                          "standard": true,
                          "labels": {
                            "en": "Place of signing",
                            "de": "Ort der Unterzeichnung",
                            "fr": "Lieu de signature",
                            "it": "Luogo della firma",
                            "es": "Lugar de la firma",
                            "nl": "Plaats van ondertekening"
                          },
                          "description": "Place where this signer signs, usually a city. For signer 2: signing.place_s2."
                        },
                        {
                          "key": "signing.place_s2",
                          "kind": "scalar",
                          "owner": "signer",
                          "required": false,
                          "signer_required": false,
                          "has_default": false,
                          "slot": 2,
                          "label": "Place of signing",
                          "type": "text",
                          "source": "lead",
                          "example": "Zürich",
                          "standard": true,
                          "labels": {
                            "en": "Place of signing",
                            "de": "Ort der Unterzeichnung",
                            "fr": "Lieu de signature",
                            "it": "Luogo della firma",
                            "es": "Lugar de la firma",
                            "nl": "Plaats van ondertekening"
                          },
                          "description": "Place where this signer signs, usually a city. For signer 2: signing.place_s2."
                        }
                      ],
                      "signing_fields": [
                        {
                          "kind": "signature",
                          "slot": 1,
                          "required": true
                        },
                        {
                          "kind": "date",
                          "slot": 1,
                          "required": true
                        },
                        {
                          "kind": "signature",
                          "slot": 2,
                          "required": true
                        },
                        {
                          "kind": "date",
                          "slot": 2,
                          "required": true
                        }
                      ]
                    }
                  },
                  "rich_text_template": {
                    "summary": "Rich-text template: two `api` placeholders, one signer",
                    "description": "\"Mandate letter\" v2: `{{person.full_name}}` and `{{address.full}}` are placeholders the author assigned to the API, so both are required. The signing date prints written out in English (`date_format: long`).",
                    "value": {
                      "template": {
                        "id": "2c7e9b14-5a3d-4f08-b6e1-9d4a0c3f7e25",
                        "name": "Mandate letter",
                        "description": "Engagement letter for a new client mandate.",
                        "status": "locked",
                        "version": 2,
                        "pages": 1,
                        "content_kind": "richtext",
                        "instantiable": true,
                        "updated_at": "2026-09-18T15:40:02.117Z"
                      },
                      "recipients": [
                        {
                          "slot": 1
                        }
                      ],
                      "field_values": [
                        {
                          "key": "person.full_name",
                          "kind": "scalar",
                          "owner": "api",
                          "required": true,
                          "label": "Full name",
                          "type": "text",
                          "source": "lead",
                          "example": "Anna Muster",
                          "standard": true,
                          "labels": {
                            "en": "Full name",
                            "de": "Vor- und Nachname",
                            "fr": "Nom complet",
                            "it": "Nome e cognome",
                            "es": "Nombre completo",
                            "nl": "Volledige naam"
                          },
                          "description": "Full name of the person as one value, exactly as it should print."
                        },
                        {
                          "key": "address.full",
                          "kind": "scalar",
                          "owner": "api",
                          "required": true,
                          "label": "Address",
                          "type": "text",
                          "source": "lead",
                          "example": "Bahnhofstrasse 1, 8001 Zürich",
                          "standard": true,
                          "labels": {
                            "en": "Address",
                            "de": "Adresse",
                            "fr": "Adresse",
                            "it": "Indirizzo",
                            "es": "Dirección",
                            "nl": "Adres"
                          },
                          "description": "The complete postal address on one line (street, postal code, city)."
                        }
                      ],
                      "signing_fields": [
                        {
                          "kind": "signature",
                          "slot": 1,
                          "required": true
                        },
                        {
                          "kind": "date",
                          "slot": 1,
                          "required": true,
                          "date_format": "long",
                          "lang": "en"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/templates/{id}/generate": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "post": {
        "tags": [
          "Templates"
        ],
        "operationId": "generateFromTemplate",
        "summary": "Fill a locked template and return the PDF — no signing request",
        "description": "Pure document generation (invoices, confirmations, letters). Enterprise-gated (402, checked before the rate limit). Three modes, one validation pass:\n\n- default → 200 `application/pdf`, nothing stored;\n- `validate_only: true` → 200 JSON dry run, no render, no row; it lists `missing_optional` (the inputs you may still supply and left empty);\n- `review: true` → 201 JSON: the PDF is rendered and persisted as a STAGED file-only document that a human confirms (POST /documents/{id}/confirm) or discards; fetch the finalized file with GET /documents/{id}/pdf.\n\nBad input is ONE 422 `template_input_invalid` listing every problem.\n\n`metadata` (your own reference) is validated in every mode (400 `invalid_metadata`) but stored only when `review: true` creates a document — the streamed default has no document to carry it.\n\nAn unknown top-level key is ignored and named in `warnings[]` on the JSON answers (validate_only, the review 201, the 422); the streamed PDF, which has no JSON body, names the same keys in the `X-Unknown-Fields` response header. 400 `unknown_field` from 2026-12-31, on the stream too.\n\nPositioned fields (a PDF template's text boxes) are never printed by generate: they are never required here — not even one the sender fills, which /instantiate requires — and never listed in `missing_optional`. A value sent for one is still type-checked.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GenerateBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The filled PDF (default; `Content-Disposition: attachment` with an ASCII `filename` and the real name as RFC 5987 `filename*=UTF-8''…`), or the dry-run result when `validate_only` is true.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              },
              "X-Unknown-Fields": {
                "description": "PDF stream only (JSON answers carry `warnings[]` instead). The top-level body keys this endpoint ignored — comma-separated, each key percent-encoded, at most 50 followed by `...`. Absent when every key is known. From 2026-12-31 such a key is refused with 400 `unknown_field`.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "send_email,sendEmails"
                  ]
                }
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "contentMediaType": "application/pdf"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidateOnlyResult"
                }
              }
            }
          },
          "201": {
            "description": "`review: true` — staged for review.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GenerateStaged"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` (\"Invalid JSON body\" when the body does not parse, which before 2026-09-24 was read as `{}` and rendered the template unfilled — an empty body is still `{}`; or body validation failed, zod message — this includes a `field_values` value that is not a string or an array of string-cell rows) or `invalid_metadata` (`problems: [{ path, message }]`, one per offending key; validated in every mode, including `validate_only`).",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "$ref": "#/components/schemas/MetadataInvalid"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/EnterpriseRequired"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`template_unlocked` (lock the template first) or `not_rendered` (a PDF-backed template with no stored source).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`template_input_invalid` — every problem with `field_values`, in one response.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TemplateInputInvalid"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`insert_failed` — the staged document row could not be written (`review: true`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`render_failed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`review_unavailable` — `review: true` needs a database migration this environment has not applied; nothing was left behind.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/templates/{id}/instantiate": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "post": {
        "tags": [
          "Templates"
        ],
        "operationId": "instantiateTemplate",
        "summary": "Create a document from a locked template and send it for signature",
        "description": "Creates a fresh document from the template, one signing request per recipient slot (parallel or sequential), emails the pending signers (unless `send_emails: false`) and returns every signing URL. Enterprise-gated (402, checked before the rate limit).\n\nRecipients may carry the SMS second factor (`phone_e164` + `require_sms_verification`, `sms_gate`), a `reference`, a `company` and a `job_title` exactly as signers on POST /signing-requests; the recipient object is strict, so a key it does not know (e.g. `phone`) answers 400 `unknown_recipient_field` rather than being dropped. At the workspace's monthly document cap the call answers 402 `tier_required` (`meta: { tier, cap, used }`, as on POST /signing-requests) before anything is written, `review: true` included — a staged instance counts from the moment it exists. A direct send whose SMS recipients outnumber the codes left this month answers 402 `sms_allowance_exhausted` before anything is written (`review: true` is checked at confirm). An unknown top-level key is ignored and named in `warnings[]` (400 `unknown_field` from 2026-12-31). `metadata` (your own reference) is stored on the document and echoed back.\n\n- default → 200 with every signing URL (InstantiateResult).\n- `validate_only: true` → 200 dry run; creates nothing and never consumes an `Idempotency-Key`.\n- A rich-text template is rendered with the values before anything is written; if that fails the call answers 502 `render_failed` and creates nothing (it never falls back to the unfilled template).\n- `review: true` → 201: rows are staged (inert) until POST /documents/{id}/confirm; the 201 deliberately carries no signing URLs.\n- All or nothing: the document, every signing request, its fields and its audit row are written before any invitation or webhook goes out. A failed write deletes everything this call created (the rendered PDF of a rich-text template too; the template's own PDF never) and answers 500 `db_failed` with `meta.rolled_back`; nobody is contacted.\n\nSupports `Idempotency-Key` exactly like POST /signing-requests (replay → the cached 201/200 with `Idempotent-Replayed: true`; reuse with a different body → 422; concurrent → 409 with `Retry-After`).",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InstantiateBody"
              },
              "examples": {
                "send_now": {
                  "summary": "Send now: two representatives, sequential, Sender / API values",
                  "description": "For the PDF template \"Power of attorney (company)\" v3. `company.legal_name` and `company.tax_id` are its required Sender / API values; the Signer boxes `signing.place` / `signing.place_s2` are left to the signers. Representative 1 must verify by SMS. `job_title` and `company` print under each signature.",
                  "value": {
                    "recipients": [
                      {
                        "slot": 1,
                        "email": "anna.keller@nordlicht-logistik.de",
                        "name": "Anna Keller",
                        "phone_e164": "+41791234567",
                        "require_sms_verification": true,
                        "reference": "rep-1",
                        "company": "Nordlicht Logistik GmbH",
                        "job_title": "Geschäftsführerin"
                      },
                      {
                        "slot": 2,
                        "email": "marco.rossi@nordlicht-logistik.de",
                        "name": "Marco Rossi",
                        "company": "Nordlicht Logistik GmbH",
                        "job_title": "Prokurist"
                      }
                    ],
                    "field_values": {
                      "company.legal_name": "Nordlicht Logistik GmbH",
                      "company.tax_id": "DE298765432-00001"
                    },
                    "signing_mode": "sequential",
                    "locale": "de",
                    "metadata": {
                      "case_id": "ZT-2026-0142"
                    }
                  }
                },
                "stage_for_review": {
                  "summary": "Stage for review (`review: true`)",
                  "value": {
                    "recipients": [
                      {
                        "slot": 1,
                        "email": "anna.keller@nordlicht-logistik.de",
                        "name": "Anna Keller",
                        "phone_e164": "+41791234567",
                        "require_sms_verification": true,
                        "reference": "rep-1",
                        "company": "Nordlicht Logistik GmbH",
                        "job_title": "Geschäftsführerin"
                      },
                      {
                        "slot": 2,
                        "email": "marco.rossi@nordlicht-logistik.de",
                        "name": "Marco Rossi",
                        "company": "Nordlicht Logistik GmbH",
                        "job_title": "Prokurist"
                      }
                    ],
                    "field_values": {
                      "company.legal_name": "Nordlicht Logistik GmbH",
                      "company.tax_id": "DE298765432-00001"
                    },
                    "signing_mode": "sequential",
                    "locale": "de",
                    "metadata": {
                      "case_id": "ZT-2026-0142"
                    },
                    "review": true
                  }
                },
                "dry_run": {
                  "summary": "Dry run (`validate_only: true`)",
                  "value": {
                    "recipients": [
                      {
                        "slot": 1,
                        "email": "anna.keller@nordlicht-logistik.de",
                        "name": "Anna Keller",
                        "phone_e164": "+41791234567",
                        "require_sms_verification": true,
                        "reference": "rep-1",
                        "company": "Nordlicht Logistik GmbH",
                        "job_title": "Geschäftsführerin"
                      },
                      {
                        "slot": 2,
                        "email": "marco.rossi@nordlicht-logistik.de",
                        "name": "Marco Rossi",
                        "company": "Nordlicht Logistik GmbH",
                        "job_title": "Prokurist"
                      }
                    ],
                    "field_values": {
                      "company.legal_name": "Nordlicht Logistik GmbH",
                      "company.tax_id": "DE298765432-00001"
                    },
                    "signing_mode": "sequential",
                    "locale": "de",
                    "metadata": {
                      "case_id": "ZT-2026-0142"
                    },
                    "validate_only": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sent now (the default): InstantiateResult, which has no `template_id`. Or, with `validate_only: true`, the dry-run result (ValidateOnlyResult, which has one).",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/Idempotent-Replayed"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/InstantiateResult"
                    },
                    {
                      "$ref": "#/components/schemas/ValidateOnlyResult"
                    }
                  ]
                },
                "examples": {
                  "sent": {
                    "summary": "Sent now (the direct send; no `template_id`)",
                    "description": "Sequential: representative 2 waits (`emailed: false`) and is invited when representative 1 has signed. Representative 1 verifies by SMS, hence `signature_level: AES`. `signing_url` is on the workspace's wesign.now host, the same link the invitation email carries.",
                    "value": {
                      "document_id": "f2a9c4e1-7b3d-4e58-a1c6-0d9e8b7f6a53",
                      "metadata": {
                        "case_id": "ZT-2026-0142"
                      },
                      "template_version": 3,
                      "signing_mode": "sequential",
                      "recipients": [
                        {
                          "slot": 1,
                          "email": "anna.keller@nordlicht-logistik.de",
                          "signing_request_id": "3b8e1f07-9a2c-4d65-b0f3-6c1e8a9d2b47",
                          "signing_url": "https://mustertreuhand.wesign.now/de/sign/7c2e9a4f1b8d3e6a0f5c2b9d4e7a1c3f",
                          "emailed": true,
                          "require_sms_verification": true,
                          "phone_masked": "•••• •••• 4567",
                          "sms_verified_at": null,
                          "sms_gate": "before_sign",
                          "signature_level": "AES",
                          "reference": "rep-1",
                          "company": "Nordlicht Logistik GmbH",
                          "job_title": "Geschäftsführerin"
                        },
                        {
                          "slot": 2,
                          "email": "marco.rossi@nordlicht-logistik.de",
                          "signing_request_id": "a41c6d28-0e7b-4f93-8d25-1b6f3e9c7a08",
                          "signing_url": "https://mustertreuhand.wesign.now/de/sign/e1d4b7a0c3f6928b5e1a4d7c0b3f6e92",
                          "emailed": false,
                          "require_sms_verification": false,
                          "phone_masked": null,
                          "sms_verified_at": null,
                          "sms_gate": "before_sign",
                          "signature_level": "SES",
                          "reference": null,
                          "company": "Nordlicht Logistik GmbH",
                          "job_title": "Prokurist"
                        }
                      ],
                      "field_values_echoed": {
                        "company.legal_name": "Nordlicht Logistik GmbH",
                        "company.tax_id": "DE298765432-00001"
                      },
                      "warnings": []
                    }
                  },
                  "validate_only_ok": {
                    "summary": "Dry run: the real call would pass",
                    "description": "`missing_optional` names the Signer boxes your app could still prefill. Nothing was created.",
                    "value": {
                      "ok": true,
                      "template_id": "8d3f1a52-6b0e-4c7a-9f21-3e5d7c9b1a04",
                      "template_version": 3,
                      "warnings": [],
                      "problems": [],
                      "missing_optional": [
                        {
                          "key": "signing.place",
                          "label": "Place of signing",
                          "owner": "signer",
                          "type": "text",
                          "standard": true,
                          "example": "Zürich",
                          "labels": {
                            "en": "Place of signing",
                            "de": "Ort der Unterzeichnung",
                            "fr": "Lieu de signature",
                            "it": "Luogo della firma",
                            "es": "Lugar de la firma",
                            "nl": "Plaats van ondertekening"
                          },
                          "slot": 1
                        },
                        {
                          "key": "signing.place_s2",
                          "label": "Place of signing",
                          "owner": "signer",
                          "type": "text",
                          "standard": true,
                          "example": "Zürich",
                          "labels": {
                            "en": "Place of signing",
                            "de": "Ort der Unterzeichnung",
                            "fr": "Lieu de signature",
                            "it": "Luogo della firma",
                            "es": "Lugar de la firma",
                            "nl": "Plaats van ondertekening"
                          },
                          "slot": 2
                        }
                      ]
                    }
                  },
                  "validate_only_sms_refused": {
                    "summary": "Dry run: the real call would answer 402 `sms_allowance_exhausted`",
                    "description": "HTTP 200 with `ok: false`: representative 1 must verify by SMS and the workspace has no SMS codes left this month. Read `ok`, not only the status.",
                    "value": {
                      "ok": false,
                      "template_id": "8d3f1a52-6b0e-4c7a-9f21-3e5d7c9b1a04",
                      "template_version": 3,
                      "warnings": [],
                      "problems": [
                        {
                          "field": "recipients",
                          "code": "sms_allowance_exhausted",
                          "message": "The real call would be refused with 402 sms_allowance_exhausted: 1 signer must verify by SMS, the workspace has 0 of 300 SMS codes left this month (resets 2026-10-01T00:00:00.000Z).",
                          "sms_allowance": {
                            "monthly": 300,
                            "used": 300,
                            "remaining": 0,
                            "resets_at": "2026-10-01T00:00:00.000Z"
                          },
                          "required": 1
                        }
                      ],
                      "missing_optional": [
                        {
                          "key": "signing.place",
                          "label": "Place of signing",
                          "owner": "signer",
                          "type": "text",
                          "standard": true,
                          "example": "Zürich",
                          "labels": {
                            "en": "Place of signing",
                            "de": "Ort der Unterzeichnung",
                            "fr": "Lieu de signature",
                            "it": "Luogo della firma",
                            "es": "Lugar de la firma",
                            "nl": "Plaats van ondertekening"
                          },
                          "slot": 1
                        },
                        {
                          "key": "signing.place_s2",
                          "label": "Place of signing",
                          "owner": "signer",
                          "type": "text",
                          "standard": true,
                          "example": "Zürich",
                          "labels": {
                            "en": "Place of signing",
                            "de": "Ort der Unterzeichnung",
                            "fr": "Lieu de signature",
                            "it": "Luogo della firma",
                            "es": "Lugar de la firma",
                            "nl": "Plaats van ondertekening"
                          },
                          "slot": 2
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "`review: true` — staged for review; nothing has been sent.",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/Idempotent-Replayed"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InstantiateStaged"
                },
                "examples": {
                  "staged": {
                    "summary": "Staged for review: no signing URLs until a confirm",
                    "description": "`review_url` is a page in the web app for a signed-in member of your workspace. Confirm or discard it there or with POST /documents/{id}/confirm and /discard.",
                    "value": {
                      "document_id": "c6e1a8f3-2d5b-4b07-9e4a-8f1c3d6b0a29",
                      "metadata": {
                        "case_id": "ZT-2026-0142"
                      },
                      "template_id": "8d3f1a52-6b0e-4c7a-9f21-3e5d7c9b1a04",
                      "template_version": 3,
                      "status": "staged",
                      "signing_mode": "sequential",
                      "send_emails": true,
                      "expires_in_days": 14,
                      "review_url": "https://www.letssign.now/de/documents/c6e1a8f3-2d5b-4b07-9e4a-8f1c3d6b0a29/review",
                      "review_expires_at": "2026-10-08T14:05:31.207Z",
                      "recipients": [
                        {
                          "slot": 1,
                          "email": "anna.keller@nordlicht-logistik.de",
                          "signing_request_id": "d9f2b6a1-4c8e-4e3d-a7b0-2e5f8c1d9a64",
                          "require_sms_verification": true,
                          "phone_masked": "•••• •••• 4567",
                          "sms_verified_at": null,
                          "sms_gate": "before_sign",
                          "signature_level": "AES",
                          "reference": "rep-1",
                          "company": "Nordlicht Logistik GmbH",
                          "job_title": "Geschäftsführerin"
                        },
                        {
                          "slot": 2,
                          "email": "marco.rossi@nordlicht-logistik.de",
                          "signing_request_id": "17a5c3e9-8b2d-4f6a-9c1e-5d3b7a0f2e81",
                          "require_sms_verification": false,
                          "phone_masked": null,
                          "sms_verified_at": null,
                          "sms_gate": "before_sign",
                          "signature_level": "SES",
                          "reference": null,
                          "company": "Nordlicht Logistik GmbH",
                          "job_title": "Prokurist"
                        }
                      ],
                      "field_values_echoed": {
                        "company.legal_name": "Nordlicht Logistik GmbH",
                        "company.tax_id": "DE298765432-00001"
                      },
                      "warnings": []
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` (\"Invalid JSON body\" when the body does not parse, which before 2026-09-24 was read as `{}`; or body validation failed, zod message — this includes `require_sms_verification: true` without `phone_e164`, reported at recipients[i].phone_e164 with message 'phone_e164 is required when require_sms_verification is true', and `sms_gate: before_view` without it), `unknown_recipient_field` (a recipient carries a key other than slot, email, name, phone_e164, require_sms_verification, sms_gate, reference, company, job_title — e.g. `phone` — it is never dropped silently; `meta.recipients` = [{ index, fields }] for every offending recipient, `meta.accepted` = the accepted key list), `invalid_metadata` (`problems: [{ path, message }]`, one per offending key), `missing_recipients` (`meta.slots` lists the slots that have fields but no recipient), `pinned-slot-mismatch` (with `slot`: a recipient of a slot GET /templates/{id} publishes as `pinned: \"email\"` is not its `pinned_email`), or `invalid_idempotency_key`. A `validate_only` dry run surfaces every one of these too.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "$ref": "#/components/schemas/MetadataInvalid"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "`enterprise-required` (the workspace lacks template channels; checked before the rate limit), `tier_required` (the monthly document cap is reached, `meta: { tier, cap, used }`; staged or not; checked before the SMS allowance) or `sms_allowance_exhausted` (direct send only: the recipients who must verify by SMS outnumber the codes left this month). Nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/EnterpriseRequiredBody"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "$ref": "#/components/schemas/SmsAllowanceExhausted"
                    }
                  ]
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "description": "`not_found` (unknown id or another workspace) or `version_not_found` (the template exists but `version` has no snapshot; `meta.version`, `meta.current_version`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`template_unlocked`, `interactive-table-requires-sequential` (an interactive quote table with several recipients needs `signing_mode: sequential` or explicit table rows), or `idempotency_in_progress` (with `Retry-After`).",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/Retry-After"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`signer_without_signature` (a recipient slot has no signature/initial field), `template_input_invalid` (see schema), or `idempotency_key_reuse`.",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/TemplateInputInvalid"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                },
                "examples": {
                  "template_input_invalid": {
                    "summary": "`template_input_invalid`: a required Sender / API value is missing",
                    "description": "`company.tax_id` was sent as `company.taxid`. The unknown key is only a warning; the missing required key is the problem that blocks the call. `docs` is a page in the web app for a signed-in workspace member, not an API resource.",
                    "value": {
                      "error": "1 problem with this request.",
                      "code": "template_input_invalid",
                      "template_id": "8d3f1a52-6b0e-4c7a-9f21-3e5d7c9b1a04",
                      "template_name": "Power of attorney (company)",
                      "template_version": 3,
                      "problems": [
                        {
                          "field": "company.tax_id",
                          "label": "Company tax ID",
                          "code": "field_required",
                          "message": "\"Company tax ID\" is required by this template but was not supplied."
                        }
                      ],
                      "warnings": [
                        {
                          "field": "company.taxid",
                          "code": "unknown_field",
                          "message": "\"company.taxid\" is not an input of this template and was ignored."
                        }
                      ],
                      "docs": "https://www.letssign.now/de/settings/api"
                    }
                  },
                  "template_input_invalid_dry_run": {
                    "summary": "The same body with `validate_only: true`",
                    "description": "A dry run's 422 also lists `missing_optional`; a real call's 422 never does.",
                    "value": {
                      "error": "1 problem with this request.",
                      "code": "template_input_invalid",
                      "template_id": "8d3f1a52-6b0e-4c7a-9f21-3e5d7c9b1a04",
                      "template_name": "Power of attorney (company)",
                      "template_version": 3,
                      "problems": [
                        {
                          "field": "company.tax_id",
                          "label": "Company tax ID",
                          "code": "field_required",
                          "message": "\"Company tax ID\" is required by this template but was not supplied."
                        }
                      ],
                      "warnings": [
                        {
                          "field": "company.taxid",
                          "code": "unknown_field",
                          "message": "\"company.taxid\" is not an input of this template and was ignored."
                        }
                      ],
                      "docs": "https://www.letssign.now/de/settings/api",
                      "missing_optional": [
                        {
                          "key": "signing.place",
                          "label": "Place of signing",
                          "owner": "signer",
                          "type": "text",
                          "standard": true,
                          "example": "Zürich",
                          "labels": {
                            "en": "Place of signing",
                            "de": "Ort der Unterzeichnung",
                            "fr": "Lieu de signature",
                            "it": "Luogo della firma",
                            "es": "Lugar de la firma",
                            "nl": "Plaats van ondertekening"
                          },
                          "slot": 1
                        },
                        {
                          "key": "signing.place_s2",
                          "label": "Place of signing",
                          "owner": "signer",
                          "type": "text",
                          "standard": true,
                          "example": "Zürich",
                          "labels": {
                            "en": "Place of signing",
                            "de": "Ort der Unterzeichnung",
                            "fr": "Lieu de signature",
                            "it": "Luogo della firma",
                            "es": "Lugar de la firma",
                            "nl": "Plaats van ondertekening"
                          },
                          "slot": 2
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`db_failed` — a write failed (the document, a signing request, its fields, or the staged audit row). Nothing was sent. `meta.rolled_back: true`: everything this call created was deleted and `error` ends \"Nothing was created or sent; retry the request.\"; `meta.rolled_back: false` (rare): the partly created document is named in `meta.document_id`, its open signing requests are withdrawn where possible, and we are alerted. A failed field insert answers this too (before, it left the recipient with nothing to sign). The `Idempotency-Key` is released, so retrying the same body is safe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`render_failed` — the filled PDF of a rich-text template could not be rendered or stored. Nothing was created or sent (no document, no signing request, no email, no webhook) and the `Idempotency-Key` is released: retry the same call. Same body as the confirm 502: `{ error, code }`. A PDF template is never rendered and never answers this.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/embedded/sign-sessions": {
      "post": {
        "tags": [
          "Embedded"
        ],
        "operationId": "createEmbeddedSignSession",
        "summary": "Mint an embedded signing session",
        "description": "Exchange an existing signing request for a short-lived, single-signer session and get back an `embed_url` to render in an iframe or a mobile webview. Call this from your BACKEND — an API key must never reach a browser. The session is bound to one workspace, one signing request and one ancestor origin, and lives `ttl_seconds` (60–900, default 300): minutes, not hours. A leaked session URL therefore cannot be widened, replayed for long, or hosted anywhere else.\n\n**Embedding is a permission, not a default.** `origin` must already be registered on the calling key's allowed origins, or the call is refused with 400 `origin_not_allowed`; the frame is then served with `Content-Security-Policy: frame-ancestors <that origin>`, so the browser itself refuses to render it under any other parent. Ask support (or use Settings → API) to register an origin.\n\n**What your page's CSP needs.** One directive: `frame-src https://<slug>.letssign.now https://<slug>.wesign.now` — the workspace's signing subdomain on either root; the returned `embed_url` tells you which one this workspace uses. Nothing else has to be relaxed. The frame is a separate document that loads its scripts, styles, fonts, images and PDF rendering from its own origin, and your CSP does not govern a child document's subresources. If you sandbox the iframe, it needs at least `allow-scripts allow-same-origin allow-forms`.\n\n**postMessage (schema v1).** The frame posts to `window.parent` with `targetOrigin` set to the session's `origin`, never `*`: `{ \"v\": 1, \"type\": …, \"session_id\": …, \"signing_request_id\": …, \"at\": \"<ISO 8601>\", \"detail\"?: \"…\" }`, where `type` is one of `wesign.ready`, `wesign.viewed`, `wesign.signed`, `wesign.declined`, `wesign.expired`, `wesign.error`. Check `event.origin` against the frame's origin and `data.v === 1` before trusting a message. These are UX signals so you can advance a case without waiting: the server-side **webhook remains the source of truth**, and your data model must not let the two contradict each other — treat `wesign.signed` as \"show the next step\", never as \"the document is signed\".\n\nEnterprise-gated (`embedded_signing_enabled`). An `embedded`-scoped key is sufficient (a `full` key satisfies it too), so the key that mints frames need not be able to do anything else.\n\n**SMS before viewing.** A signing request with `sms_gate: before_view` cannot be signed in the frame yet: until its code is verified the frame shows the \"session not available\" card and posts `wesign.error` with detail `sms_required`. `before_sign` (the default) works in the frame, SMS step included. A decline reason reaches your backend as `reason` on the `signing_request.declined` webhook, never through postMessage.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmbedSignSessionCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Session minted. Render `embed_url` in the frame; it stops working at `expires_at`.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmbedSignSessionCreated"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — body validation failed (including `ttl_seconds` outside 60–900, an unknown `locale`, or a rejected `theme` value; out-of-range TTLs are refused, never silently clamped). `invalid_origin` — `origin` is not an https origin of the form `https://host[:port]` (a path, query, credentials or a wildcard are all rejected). `origin_not_allowed` — the origin is well-formed but is not registered on this API key; the message lists the origins that are.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/EnterpriseRequired"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`request_not_signable` — the signing request is not `pending` or `viewed` (already signed, declined, withdrawn), or it is past its own `expires_at`. Nothing was minted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "description": "`db_failed` — the lookup or the session insert failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/EmbeddingUnavailable"
          }
        }
      }
    },
    "/embedded/sign-sessions/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "get": {
        "tags": [
          "Embedded"
        ],
        "operationId": "getEmbeddedSignSession",
        "summary": "Status of an embedded signing session",
        "description": "Whether the permit is still good and whether the frame was ever opened. Scoped to the key's workspace: another tenant's session id answers 404, never 403. Not Enterprise-gated — a workspace must always be able to inspect sessions it already minted.\n\nThis reports the lifecycle of the SESSION, not of the document. A session can read `active` while its signing request has already been signed; poll `GET /signing-requests/{id}` (or read the webhook) for that.",
        "responses": {
          "200": {
            "description": "The session.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmbedSignSession"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/EmbeddingUnavailable"
          }
        }
      },
      "delete": {
        "tags": [
          "Embedded"
        ],
        "operationId": "revokeEmbeddedSignSession",
        "summary": "Revoke an embedded signing session",
        "description": "Kill the permit now, before it expires on its own — the frame stops rendering on its next load. Idempotent: revoking an already-revoked or already-expired session answers the same `{ \"ok\": true }`, so a teardown path never has to care what state the session was in. Scoped to the key's workspace and not Enterprise-gated: revocation is a safety valve and must never depend on billing.\n\nRevoking a session does NOT withdraw the signing request — use `POST /signing-requests/{id}/withdraw` for that. It only takes back the right to sign it inside your page.",
        "responses": {
          "200": {
            "description": "Revoked (or already was).",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/EmbeddingUnavailable"
          }
        }
      }
    },
    "/embedded/sessions": {
      "post": {
        "tags": [
          "Retired"
        ],
        "operationId": "createEmbeddedSessionRetired",
        "summary": "Retired — embedded session minting",
        "description": "Retired 2026-07-31 with the guest placement flow it relied on. Answers a stable 410 without authenticating. Embedded signing itself is live on a different design — use POST /embedded/sign-sessions (note the hyphen).",
        "deprecated": true,
        "security": [],
        "responses": {
          "410": {
            "description": "`embedded_sessions_retired`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/embedded/sessions/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/id"
        }
      ],
      "get": {
        "tags": [
          "Embedded"
        ],
        "operationId": "getEmbeddedSession",
        "summary": "Status of an embedded session",
        "description": "Requires a key with the `embedded` scope (a `full` key satisfies it).",
        "responses": {
          "200": {
            "description": "The session.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EmbeddedSession"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "delete": {
        "tags": [
          "Embedded"
        ],
        "operationId": "revokeEmbeddedSession",
        "summary": "Revoke an embedded session",
        "description": "Marks the session revoked and blocks its link. Idempotent for already-revoked sessions; a consumed session cannot be revoked. Requires the `embedded` scope.",
        "responses": {
          "200": {
            "description": "Revoked.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimit-Limit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimit-Remaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimit-Reset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "status"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "status": {
                      "type": "string",
                      "const": "revoked"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/IpNotAllowed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "`already_consumed` — the session was used and cannot be revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "webhooks": {
    "signing_request.sent": {
      "post": {
        "summary": "An invite went out",
        "description": "Emitted when a signer is invited. Once per document for its first dispatched signer (from POST /signing-requests, POST /templates/{id}/instantiate, a dashboard send, a template dispatch or a confirmed review) — not again for the other signers of a parallel send. A dashboard send scheduled with Send Later emits when it goes out, not when it is scheduled, in the same body. And once per signer a sequential document releases when the previous signer has signed (`source: \"sequential_release\"`, with `occurred_at` and `emailed`; an envelope emits one per document of the released signer), whatever sent the document. The release is emitted before the previous signer's `signing_request.signed`, so the two may arrive in either order. No payload carries a signing link: use the `signing_url` / `signingUrl` the create call returned, or GET /signing-requests/{id}.",
        "parameters": [
          {
            "$ref": "#/components/parameters/X-WeSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event-Id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "description": "The full body (a hook with `payload: \"full\"`, the default), or the minimal body (a hook with `payload: \"minimal\"`).",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/SigningRequestSentEvent"
                  },
                  {
                    "$ref": "#/components/schemas/MinimalSigningRequestEvent"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "$ref": "#/components/responses/WebhookAck"
          }
        }
      }
    },
    "signing_request.viewed": {
      "post": {
        "summary": "The signer opened the document for the first time",
        "description": "The signer's browser loaded the document for the first time (status `pending` → `viewed`), on the signing page or in an embedded frame. Exactly once per request. Opening the link alone does not count: a plain fetch of the link (what most mail scanners do) does not trigger it, but a scanner that renders the page in a full browser can. With `sms_gate: before_view` it fires only after the code is verified. Not guaranteed before `signing_request.signed` (a phone showing the fallback for documents without page images fires it only when the signer opens the PDF). Queued signers never emit it.",
        "parameters": [
          {
            "$ref": "#/components/parameters/X-WeSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event-Id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "description": "The full body (a hook with `payload: \"full\"`, the default), or the minimal body (a hook with `payload: \"minimal\"`).",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/SigningRequestViewedEvent"
                  },
                  {
                    "$ref": "#/components/schemas/MinimalSigningRequestEvent"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "$ref": "#/components/responses/WebhookAck"
          }
        }
      }
    },
    "signing_request.signed": {
      "post": {
        "summary": "A signer completed their signature",
        "parameters": [
          {
            "$ref": "#/components/parameters/X-WeSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event-Id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "description": "The full body (a hook with `payload: \"full\"`, the default), or the minimal body (a hook with `payload: \"minimal\"`).",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/SigningRequestSignedEvent"
                  },
                  {
                    "$ref": "#/components/schemas/MinimalSigningRequestEvent"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "$ref": "#/components/responses/WebhookAck"
          }
        },
        "description": "A signer's signature was sealed. `sha256` is the PDF after this seal. `signer` is the same block as on the signer events (SigningRequestEventSigner): email, stored name, role, slot, your reference and the company and function you stated."
      }
    },
    "signing_request.declined": {
      "post": {
        "summary": "The signer declined",
        "description": "Once per request, when the signer declines on the signing page (hosted or embedded). The document can no longer complete. `reason` is what the signer typed (first 1000 characters).",
        "parameters": [
          {
            "$ref": "#/components/parameters/X-WeSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event-Id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "description": "The full body (a hook with `payload: \"full\"`, the default), or the minimal body (a hook with `payload: \"minimal\"`).",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/SigningRequestDeclinedEvent"
                  },
                  {
                    "$ref": "#/components/schemas/MinimalSigningRequestEvent"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "$ref": "#/components/responses/WebhookAck"
          }
        }
      }
    },
    "signing_request.withdrawn": {
      "post": {
        "summary": "A signing request was withdrawn",
        "description": "Once per request pulled out of flight: by your key (`withdrawn_by: api`), by a user in the dashboard (`sender`), or by a dashboard envelope withdraw, which emits one per pending, viewed or queued request of the envelope. Cancelling a Send-Later request before it went out and discarding a staged instance (`template_instance.discarded`) do not emit it.",
        "parameters": [
          {
            "$ref": "#/components/parameters/X-WeSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event-Id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "description": "The full body (a hook with `payload: \"full\"`, the default), or the minimal body (a hook with `payload: \"minimal\"`).",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/SigningRequestWithdrawnEvent"
                  },
                  {
                    "$ref": "#/components/schemas/MinimalSigningRequestEvent"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "$ref": "#/components/responses/WebhookAck"
          }
        }
      }
    },
    "signing_request.expired": {
      "post": {
        "summary": "A signing request passed its expires_at",
        "description": "Emitted by the hourly expiry cron the moment a `pending` / `viewed` request passes its `expires_at` and its status flips to `expired` — exactly once per request (the status flip is the guard: a row can make that transition only once). `queued` sequential followers behind an expired signer do not emit; they never held a live link. Requests found more than 7 days past `expires_at` (a backlog after an outage) are expired without a delivery. The document never completes: `/signed` and `/audit-trail` keep answering 409 `not_complete`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/X-WeSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event-Id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "description": "The full body (a hook with `payload: \"full\"`, the default), or the minimal body (a hook with `payload: \"minimal\"`).",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/SigningRequestExpiredEvent"
                  },
                  {
                    "$ref": "#/components/schemas/MinimalSigningRequestEvent"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "$ref": "#/components/responses/WebhookAck"
          }
        }
      }
    },
    "signing_request.sms_sent": {
      "post": {
        "summary": "An SMS verification code was sent",
        "description": "The SMS provider accepted a verification code for this signer, sent in the request's `locale` (en, de, fr, it, es or nl). One event per code, resends included (`attempt` counts them); a resend within the code's 10-minute lifetime delivers the same code and still counts against the allowance. Not the invitation SMS of `channel` sms/both.",
        "parameters": [
          {
            "$ref": "#/components/parameters/X-WeSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event-Id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "description": "The full body (a hook with `payload: \"full\"`, the default), or the minimal body (a hook with `payload: \"minimal\"`).",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/SigningRequestSmsSentEvent"
                  },
                  {
                    "$ref": "#/components/schemas/MinimalSigningRequestEvent"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "$ref": "#/components/responses/WebhookAck"
          }
        }
      }
    },
    "signing_request.sms_failed": {
      "post": {
        "summary": "An SMS verification code could not be sent",
        "description": "The signer asked for a code and none went out; one event per refused request, the moment it happens — so a landline, a blocked destination or a used-up allowance is visible at once. `reason` is a stable public code (see the schema).",
        "parameters": [
          {
            "$ref": "#/components/parameters/X-WeSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event-Id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "description": "The full body (a hook with `payload: \"full\"`, the default), or the minimal body (a hook with `payload: \"minimal\"`).",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/SigningRequestSmsFailedEvent"
                  },
                  {
                    "$ref": "#/components/schemas/MinimalSigningRequestEvent"
                  }
                ]
              },
              "example": {
                "event": "signing_request.sms_failed",
                "event_id": "evt_7d1e0c9b3a5f4e2d8c6b1a0f9e8d7c6b",
                "created_at": "2026-10-02T08:13:52.031Z",
                "workspace_id": "6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
                "metadata": {
                  "case_id": "CT-2026-0412"
                },
                "signing_request_id": "5b2e8f41-7c3a-4d19-9e60-2a8b1c4d7e93",
                "document_id": "c4a1d9e2-3b7f-4e85-a0c6-9d2e1f3b5a78",
                "signer": {
                  "email": "anna.muster@example.ch",
                  "name": "Anna Muster",
                  "role": "client",
                  "slot": null,
                  "reference": "person-8812",
                  "company": null,
                  "job_title": null
                },
                "occurred_at": "2026-10-02T08:13:51.990Z",
                "reason": "landline",
                "phone_masked": "•••• •••• 4567",
                "sms_allowance": {
                  "monthly": 300,
                  "used": 42,
                  "remaining": 258,
                  "resets_at": "2026-11-01T00:00:00.000Z"
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "$ref": "#/components/responses/WebhookAck"
          }
        }
      }
    },
    "signing_request.sms_verified": {
      "post": {
        "summary": "The signer verified their phone",
        "description": "The signer entered the correct code. `sms_verified_at` is the stamp the AES signature relies on. Once per request.",
        "parameters": [
          {
            "$ref": "#/components/parameters/X-WeSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event-Id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "description": "The full body (a hook with `payload: \"full\"`, the default), or the minimal body (a hook with `payload: \"minimal\"`).",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/SigningRequestSmsVerifiedEvent"
                  },
                  {
                    "$ref": "#/components/schemas/MinimalSigningRequestEvent"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "$ref": "#/components/responses/WebhookAck"
          }
        }
      }
    },
    "document.completed": {
      "post": {
        "summary": "Every signer has signed",
        "description": "Fired once per document, right after the `signing_request.signed` event of the final signer, when EVERY signer row is `signed` — the same rule that makes GET /documents/{id} report `status: signed` and lets `/signed` + `/audit-trail` serve. A document with a declined, withdrawn or expired signer never fires it (its other signers' `signing_request.signed` events still do). `signed_pdf_url` and `audit_trail_url` are key-authenticated API URLs on the canonical host (GET /documents/{id}/signed and GET /documents/{id}/audit-trail) — fetch them with the owning workspace's Bearer key. Parallel signers who sign within the same seconds are sealed one after another, so `signed_pdf_url` carries every signature, and only the signature that completed the document emits this event. Delivery is at-least-once, and a database failure at the moment of completion can still repeat the event: deduplicate on `document_id` as well as `event_id`. `signer` is the completing signer, in the same block as the signer events (SigningRequestEventSigner). Fetch both URLs when this event arrives and store the files on your side: the owning workspace can have signed PDFs deleted after a set number of days, after which `signed_pdf_url` answers 410 `deleted_by_retention` (the audit trail stays). Compare `sha256` with the PDF bytes. One event per document, after its last signer; each document of an envelope completes on its own, and there is no envelope-level event. Guide: https://www.wesign.now/docs/documents#retrieve-the-signed-document.",
        "parameters": [
          {
            "$ref": "#/components/parameters/X-WeSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event-Id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "description": "The full body (a hook with `payload: \"full\"`, the default), or the minimal body (a hook with `payload: \"minimal\"`).",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/DocumentCompletedEvent"
                  },
                  {
                    "$ref": "#/components/schemas/MinimalDocumentCompletedEvent"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "$ref": "#/components/responses/WebhookAck"
          }
        }
      }
    },
    "template_instance.staged": {
      "post": {
        "summary": "A template instance was staged for review",
        "description": "Emitted by `/templates/{id}/instantiate` (kind `esign`) and `/templates/{id}/generate` (kind `file`) when called with `review: true`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/X-WeSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event-Id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "description": "The full body (a hook with `payload: \"full\"`, the default), or the minimal body (a hook with `payload: \"minimal\"`).",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/TemplateInstanceStagedEvent"
                  },
                  {
                    "$ref": "#/components/schemas/MinimalDocumentEvent"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "$ref": "#/components/responses/WebhookAck"
          }
        }
      }
    },
    "template_instance.confirmed": {
      "post": {
        "summary": "A staged instance was confirmed",
        "description": "An e-sign confirm additionally emits one ordinary `signing_request.sent` for the first promoted recipient.",
        "parameters": [
          {
            "$ref": "#/components/parameters/X-WeSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event-Id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "description": "The full body (a hook with `payload: \"full\"`, the default), or the minimal body (a hook with `payload: \"minimal\"`).",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/TemplateInstanceConfirmedEvent"
                  },
                  {
                    "$ref": "#/components/schemas/MinimalDocumentEvent"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "$ref": "#/components/responses/WebhookAck"
          }
        }
      }
    },
    "template_instance.discarded": {
      "post": {
        "summary": "A staged instance was discarded or expired",
        "parameters": [
          {
            "$ref": "#/components/parameters/X-WeSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-WeSign-Event-Id"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Signature"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event"
          },
          {
            "$ref": "#/components/parameters/X-LetsSign-Event-Id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "description": "The full body (a hook with `payload: \"full\"`, the default), or the minimal body (a hook with `payload: \"minimal\"`).",
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/TemplateInstanceDiscardedEvent"
                  },
                  {
                    "$ref": "#/components/schemas/MinimalDocumentEvent"
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "$ref": "#/components/responses/WebhookAck"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "wsk_live_<32 hex>",
        "description": "Workspace API key, minted in Settings → API. New keys are `wsk_live_<32 hex>`; keys minted before the wesign.now rename are `lsk_live_<32 hex>` (legacy, accepted forever, never re-issued). Only a SHA-256 of the key is stored — anything not starting with one of the two prefixes is refused before the database is touched. A key belongs to exactly one workspace; every resource is scoped to it (another workspace's ids answer 404). Keys carry scopes: `full` (default, everything) or `embedded` (only /embedded/*). A key may also carry an IP allowlist (IPv4/IPv6 addresses and CIDR blocks, managed in Settings → API): when it is NON-EMPTY, a call from any other address is refused 403 `ip_not_allowed` with the observed address echoed back in `ip`. An EMPTY allowlist means UNRESTRICTED — the state of every key that has not opted in, and the opposite of the embed-origin list. The address is the one the platform observed the call arriving from; `Origin` and `X-Forwarded-For` are never used to authorise a REST call, because a server-to-server caller sets both itself."
      }
    },
    "parameters": {
      "id": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Resource id (UUID).",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "1–255 printable ASCII characters. Same key + same body replays the cached response (`Idempotent-Replayed: true`); same key + different body → 422 `idempotency_key_reuse`; a concurrent retry → 409 `idempotency_in_progress` with `Retry-After` (lock stale after 60 s). Only a created document is cached; failed calls release the key. Keys are scoped to the WORKSPACE, not to the endpoint: the same key with the same body sent to another endpoint or another template replays the first response — use one key per logical request. Kept 24 hours from the first call. A lock older than 60 s is taken over by the next retry even if the first call is still running, so do not retry a call that may still be running sooner than 60 s. Read by POST /signing-requests and POST /templates/{id}/instantiate only (never by a `validate_only` dry run). The body is hashed as sent — `metadata`, `send_emails` and a multipart `filename` included, and on POST /signing-requests the PDF bytes — so retry with the identical body: reordered keys count as a different body (422).",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 255,
          "pattern": "^[\\x20-\\x7e]+$"
        }
      },
      "X-WeSign-Signature": {
        "name": "X-WeSign-Signature",
        "in": "header",
        "required": true,
        "description": "`t=<unix seconds>,v1=<hex HMAC-SHA256(secret, `${t}.${rawBody}`)>`. Canonical name. Normally one `v1=` entry; for 24 hours after POST /hooks/{id}/rotate there are two, comma-separated, new secret first — accept the delivery if ANY entry matches.",
        "schema": {
          "type": "string",
          "pattern": "^t=\\d+(,v1=[0-9a-f]{64})+$"
        }
      },
      "X-WeSign-Event": {
        "name": "X-WeSign-Event",
        "in": "header",
        "required": true,
        "description": "The event type (same value as the body's `event`).",
        "schema": {
          "$ref": "#/components/schemas/WebhookEventType"
        }
      },
      "X-WeSign-Event-Id": {
        "name": "X-WeSign-Event-Id",
        "in": "header",
        "required": true,
        "description": "Unique delivery id `evt_<32 hex>` (same value as the body's `event_id`). Use it to de-duplicate retries.",
        "schema": {
          "type": "string",
          "pattern": "^evt_[0-9a-f]{32}$"
        }
      },
      "X-LetsSign-Signature": {
        "name": "X-LetsSign-Signature",
        "in": "header",
        "required": true,
        "description": "Byte-identical to X-WeSign-Signature (including the second `v1=` entry during a rotation window). Pre-rename name, frozen forever.",
        "schema": {
          "type": "string",
          "pattern": "^t=\\d+(,v1=[0-9a-f]{64})+$"
        }
      },
      "X-LetsSign-Event": {
        "name": "X-LetsSign-Event",
        "in": "header",
        "required": true,
        "description": "Byte-identical to X-WeSign-Event. Pre-rename name, frozen forever.",
        "schema": {
          "$ref": "#/components/schemas/WebhookEventType"
        }
      },
      "X-LetsSign-Event-Id": {
        "name": "X-LetsSign-Event-Id",
        "in": "header",
        "required": true,
        "description": "Byte-identical to X-WeSign-Event-Id. Pre-rename name, frozen forever.",
        "schema": {
          "type": "string",
          "pattern": "^evt_[0-9a-f]{32}$"
        }
      }
    },
    "headers": {
      "Retry-After": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "RateLimit-Limit": {
        "description": "Requests admitted per window for this key (60). IETF draft-ietf-httpapi-ratelimit-headers. Sent on every response of a rate-limited operation, 429s included.",
        "schema": {
          "type": "integer"
        }
      },
      "RateLimit-Policy": {
        "description": "`<limit>;w=<window seconds>`, e.g. `60;w=60`.",
        "schema": {
          "type": "string"
        }
      },
      "RateLimit-Remaining": {
        "description": "Requests left in the current window after this one. Omitted when the shared counter admitted the call but could not be read back.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "RateLimit-Reset": {
        "description": "Seconds until the current window resets. Omitted under the same condition as RateLimit-Remaining.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "Idempotent-Replayed": {
        "description": "Present (value `true`) when the response was replayed from an earlier call with the same `Idempotency-Key`.",
        "schema": {
          "type": "string",
          "const": "true"
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "`invalid_key` — Bearer token missing, malformed, unknown, revoked, or lacking the required scope. A database failure on our side can answer it too: retry a 401 on a key that worked before a few times before treating the key as wrong.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "IpNotAllowed": {
        "description": "`ip_not_allowed` — the key is valid, but it carries a non-empty IP allowlist and the caller's address is not covered by it (or could not be determined, in which case a restricted key fails closed). Distinct from 401 on purpose: the key is fine, the address is not. The body carries the observed address as a top-level `ip` (null when unknown), so the integrator can paste it into the list. An EMPTY allowlist means UNRESTRICTED — the state of every key that has not opted in — which is the opposite of the embed-origin list. The address is the one the platform observed the call arriving from; `X-Forwarded-For` is never used for the decision because a caller can set it. This gates API keys only: never the workspace UI, never the signing pages. Manage the list in Settings → API.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "address_known": {
                "summary": "The caller's address was observed and is not on the list",
                "value": {
                  "error": "This API key does not allow calls from 203.0.113.9. Add that address to the key's IP allowlist in Settings → API, or empty the list to allow any address.",
                  "code": "ip_not_allowed",
                  "ip": "203.0.113.9"
                }
              },
              "address_unknown": {
                "summary": "No trusted source for the caller's address — a restricted key fails closed",
                "value": {
                  "error": "This API key restricts which IP addresses may use it, and the caller's address could not be determined, so the call was refused. Empty the key's IP allowlist in Settings → API to allow any address.",
                  "code": "ip_not_allowed",
                  "ip": null
                }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "`not_found` — the resource does not exist or belongs to another workspace (never distinguished).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InvalidRequest": {
        "description": "`invalid_request` — body validation failed; `error` carries the first zod issue.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "`rate_limited` — more than 60 requests in the current fixed 60-second window for this API key (one bucket across all of /v1; the window opens on the first request and closes 60 s later). A refusal never consumes the window; retry after `Retry-After` seconds. If the shared counter is unreachable the same policy is enforced per compute instance, so the effective ceiling can be higher than 60 but never unlimited.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          },
          "RateLimit-Limit": {
            "$ref": "#/components/headers/RateLimit-Limit"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          },
          "RateLimit-Remaining": {
            "$ref": "#/components/headers/RateLimit-Remaining"
          },
          "RateLimit-Reset": {
            "$ref": "#/components/headers/RateLimit-Reset"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "EnterpriseRequired": {
        "description": "`enterprise-required` — the key is valid but the workspace lacks the Enterprise entitlement this route needs: template channels for `POST /templates/{id}/generate`, `POST /templates/{id}/instantiate` and `POST /documents/{id}/confirm`; embedded signing for `POST /embedded/sign-sessions`.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/EnterpriseRequiredBody"
            }
          }
        }
      },
      "EmbeddingUnavailable": {
        "description": "`embedding_unavailable` — embedded signing is not available on this deployment yet: the `embed_sign_sessions` migration (0155) has not been applied. Every embed endpoint degrades to this instead of failing in a way that reads like an integration bug. Retry later or contact support; nothing was created.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "error",
                "code"
              ],
              "properties": {
                "error": {
                  "type": "string"
                },
                "code": {
                  "type": "string",
                  "const": "embedding_unavailable"
                }
              }
            }
          }
        }
      },
      "WebhookAck": {
        "description": "Return any 2xx within 10 seconds to acknowledge. Anything else (or a timeout, or a redirect) is retried on the schedule 1m, 5m, 30m, 2h, 6h, 12h, 24h, then given up. Answer 2xx to event types you do not recognise as well: a hook with an empty `events` list — and every `callback_url` hook — receives event types added later without opting in, and a refused delivery is retried for about 45 hours."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "additionalProperties": true,
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message. May improve over time — do not pin logic to it."
          },
          "code": {
            "type": "string",
            "description": "Stable machine code. Present on every v1 error."
          },
          "meta": {
            "type": "object",
            "additionalProperties": true,
            "description": "Structured context when relevant, e.g. `{ role }`, `{ roles }`, `{ size }`, `{ tier, cap, used }`, `{ slots }`, `{ signers: [{ index, fields }], accepted }` on `unknown_signer_field`, `{ recipients: [{ index, fields }], accepted }` on `unknown_recipient_field`, `{ recipients: [{ index, fields }] }` on `conflicting_fields`, `{ fields, accepted }` on `unknown_field`, `{ status, sms_verified_at }` on `not_editable`, `{ rolled_back, document_id? }` on a `db_failed` of POST /signing-requests and POST /templates/{id}/instantiate (`role` too on POST /signing-requests when a signer's write failed). (402 `sms_allowance_exhausted` carries `sms_allowance` and `required` at the top level — see SmsAllowanceExhausted.)"
          },
          "ip": {
            "type": [
              "string",
              "null"
            ],
            "description": "Present on `ip_not_allowed` (403) only: the caller address the platform observed, or null when it could not be determined. Top-level rather than inside `meta` because it is produced by the authentication layer, before any route-level context exists."
          }
        }
      },
      "Ok": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          }
        }
      },
      "Locale": {
        "type": "string",
        "enum": [
          "en",
          "de",
          "fr",
          "it",
          "nl",
          "es"
        ]
      },
      "SigningRequestStatus": {
        "type": "string",
        "description": "Lifecycle of one signer row. `queued` = waiting its turn in a sequential document, or staged for review. `expired` is set by the hourly expiry cron once `expires_at` has passed (the same tick emits `signing_request.expired`); a link past `expires_at` stops working immediately either way. `viewed` is set the first time the signer's browser loads the document — on the signing page or in an embedded frame — from the 2026-09-24 release on (earlier, no request ever reported it); with `sms_gate: before_view` only after the code is verified. It is not guaranteed before `signed`.",
        "enum": [
          "pending",
          "viewed",
          "signed",
          "declined",
          "expired",
          "withdrawn",
          "queued"
        ]
      },
      "WebhookEventType": {
        "type": "string",
        "enum": [
          "signing_request.sent",
          "signing_request.viewed",
          "signing_request.signed",
          "signing_request.declined",
          "signing_request.withdrawn",
          "signing_request.expired",
          "signing_request.sms_sent",
          "signing_request.sms_failed",
          "signing_request.sms_verified",
          "document.completed",
          "template_instance.staged",
          "template_instance.confirmed",
          "template_instance.discarded"
        ]
      },
      "Me": {
        "type": "object",
        "required": [
          "workspace",
          "key",
          "capabilities",
          "sms_allowance"
        ],
        "properties": {
          "workspace": {
            "type": "object",
            "required": [
              "id",
              "name",
              "slug",
              "tier"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "slug": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The workspace's subdomain slug."
              },
              "tier": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "free | pro | branded | teams | enterprise."
              }
            }
          },
          "key": {
            "type": "object",
            "required": [
              "id",
              "scopes"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "scopes": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "full",
                    "embedded"
                  ]
                }
              }
            }
          },
          "capabilities": {
            "type": "object",
            "description": "What the key's workspace may do on the API. Branch on these booleans, never on workspace.tier: tier names get renamed, and comped or grandfathered workspaces carry entitlements their tier string does not describe. Workspace-level — key.scopes says what this particular key may call (an `embedded`-scoped key can see templates: true and still only call /embedded/*). New keys may be added; never closed.",
            "required": [
              "embedded_signing",
              "templates",
              "webhooks",
              "sms_verification",
              "ip_allowlist",
              "metadata"
            ],
            "properties": {
              "embedded_signing": {
                "type": "boolean",
                "description": "POST /embedded/sign-sessions is available (Enterprise today). When false it answers 402 enterprise-required."
              },
              "templates": {
                "type": "boolean",
                "description": "POST /templates/{id}/instantiate, /templates/{id}/generate and /documents/{id}/confirm are available (Enterprise today). When false they answer 402 enterprise-required."
              },
              "webhooks": {
                "type": "boolean",
                "description": "REST-hook subscriptions (/hooks) and per-request callback_url. Always true today; reported so every capability is feature-detected the same way."
              },
              "sms_verification": {
                "type": "boolean",
                "description": "Always true: SMS verification is available on every tier. How many codes are left is `sms_allowance`; this flag does not flip to false at 0 remaining."
              },
              "ip_allowlist": {
                "type": "boolean",
                "description": "API keys can be pinned to source IPs (403 ip_not_allowed otherwise). Always true today."
              },
              "metadata": {
                "type": "boolean",
                "description": "The `metadata` object is accepted on every create endpoint and echoed on webhooks and GET /documents/{id}. Always true today."
              }
            }
          },
          "sms_allowance": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/SmsAllowance"
              },
              {
                "type": "null"
              }
            ],
            "description": "This month's SMS allowance — the number a create call's 402 `sms_allowance_exhausted` is decided on, so you can warn before sending. Null only when the workspace row could not be read."
          }
        }
      },
      "FieldDefinition": {
        "type": "object",
        "required": [
          "key",
          "label",
          "source",
          "type",
          "example",
          "max_length",
          "description",
          "standard"
        ],
        "properties": {
          "key": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$",
            "maxLength": 64,
            "description": "The `field_values` key: lowercase letters, digits and underscores, in segments joined by single dots — `client_name`, `person.date_of_birth`, `company.uid`. Every segment starts with a letter; at most 64 characters (`..`, a leading or trailing dot, capitals and hyphens are invalid). A dot is part of the key, never nesting: `field_values` stays a flat map, and `person.date_of_birth` and `person_date_of_birth` are two different keys."
          },
          "label": {
            "type": [
              "string",
              "null"
            ]
          },
          "source": {
            "type": "string",
            "enum": [
              "lead",
              "api"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "text",
              "multiline",
              "email",
              "phone",
              "number",
              "currency",
              "date",
              "time",
              "enum",
              "boolean",
              "agree",
              "collection"
            ],
            "description": "The answer type, one of the types Settings → API offers. A new type is an additive change (see Versioning): treat a value you do not know like `text`. A `boolean` field takes a yes/no answer as a STRING — `\"true\"`/`\"false\"`, `\"yes\"`/`\"no\"` or a UI-locale label (`\"ja\"`, `\"nein\"`, `\"oui\"`, `\"non\"`, `\"sí\"`, `\"nee\"`…), case-insensitive; any other value (`\"X\"`, `\"1\"`, `\"on\"`) is rejected with 422 template_input_invalid / `invalid_boolean` and is never silently left empty. A raw JSON `true`/`false` fails the body schema (400 invalid_request) — send the string. A `date` field takes a day-first date (`DD.MM.YYYY`, `DD/MM/YYYY`, `DD-MM-YYYY`, `DD MM YYYY`) or `YYYY-MM-DD` and stores it as `YYYY-MM-DD`; month-first is refused (422 `invalid_date`) — see FieldValues. A `time` takes `HH:MM` or `HH.MM` (24-hour, `14:30`, `9:05`) or `h:MM AM/PM` (`2:30 PM`, `2:30pm`) and stores it as 24-hour `HH:MM`; anything else is 422 `invalid_time`. How a date or time PRINTS is the template's choice, never an input rule — see TemplateInputField.date_format / time_format."
          },
          "example": {
            "type": [
              "string",
              "null"
            ]
          },
          "max_length": {
            "type": [
              "integer",
              "null"
            ]
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Enum types only: the display labels."
          },
          "columns": {
            "type": "array",
            "description": "Collection types only.",
            "items": {
              "type": "object",
              "required": [
                "key",
                "label"
              ],
              "properties": {
                "key": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                }
              }
            }
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "standard": {
            "type": "boolean",
            "description": "True when `key` is a standard field name — see TemplateInputField.standard. Always present. A standard key also carries `labels` (the name in the six UI languages) and, where one exists, `pattern`."
          },
          "labels": {
            "$ref": "#/components/schemas/FieldLabels",
            "description": "Standard keys only: the standard name in the six UI languages. Absent on a custom key."
          },
          "pattern": {
            "type": "string",
            "description": "Standard keys only, where one exists: what a value usually looks like. A hint, never enforced."
          }
        }
      },
      "Hook": {
        "type": "object",
        "description": "A subscription as GET /hooks lists it — never carries the signing secret. Property order: id, target_url, events, enabled, created_at, payload.",
        "required": [
          "id",
          "target_url",
          "events",
          "enabled",
          "created_at",
          "payload"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "target_url": {
            "type": "string",
            "format": "uri"
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEventType"
            },
            "description": "Empty array = subscribed to every event, including event types added later (\"no filter\", not a snapshot)."
          },
          "enabled": {
            "type": "boolean",
            "description": "false when the hook is switched off: disabled in the dashboard under Developers → Webhooks, or replaced by a later POST /hooks with the same target_url. Disabled hooks are listed only with include_disabled=true and never receive deliveries. A hook created by POST /hooks is always enabled: true."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "payload": {
            "type": "string",
            "enum": [
              "full",
              "minimal"
            ],
            "description": "The body this hook receives. `full` (the default): the documented body — envelope with `workspace_id`, `metadata`, the `signer` block and every event field. `minimal`: identifiers only, no personal data — `event`, `event_id`, `created_at`, `document_id` on every event; `signing_request_id`, `role`, `reference` on signing_request.* events; `signed_pdf_url`, `sha256`, `audit_trail_url` on document.completed — sent with `User-Agent: wesign-webhooks/1` (a full hook keeps `letssign.now-webhooks/1.0`). See the Minimal* schemas."
          }
        }
      },
      "HookCreated": {
        "description": "The POST /hooks response: the hook plus its signing secret, shown once.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Hook"
          },
          {
            "type": "object",
            "required": [
              "secret",
              "replaced_hook_ids"
            ],
            "properties": {
              "secret": {
                "type": "string",
                "pattern": "^whsec_[0-9a-f]{48}$",
                "description": "Shown once, only here. Verify `X-WeSign-Signature` (or `X-LetsSign-Signature`) with it: the HMAC key is this whole string, `whsec_` prefix included, as UTF-8. Not returned by GET /hooks or any other call; lost it → POST /hooks/{id}/rotate, or re-POST the same target_url (a new hook, a new secret)."
              },
              "replaced_hook_ids": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid"
                },
                "description": "The hooks this call switched off because they targeted the same URL: usually none, otherwise every older active workspace-wide hook on it (a dashboard-created one included). They no longer appear in GET /hooks and receive nothing more; their deliveries still being retried, for events this hook subscribes to, moved to this hook, keeping their `event_id`."
              }
            }
          }
        ]
      },
      "HookCreate": {
        "type": "object",
        "required": [
          "target_url"
        ],
        "properties": {
          "target_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Public **https** URL we POST to. Anything but `https:` answers 400 `https_required`; a host that is or resolves to a private, loopback, link-local (169.254/16, fe80::/10), CGNAT, multicast or otherwise reserved address — or `localhost`, `*.local`, `*.internal`, or a name that does not resolve — answers 400 `url_not_public`. Checked again at every delivery (DNS can change). Hooks registered over http before the 2026-10-02 release keep delivering."
          },
          "events": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "$ref": "#/components/schemas/WebhookEventType"
            },
            "description": "Omit (or send `[]`) to receive every event — including event types added later: an empty list means \"no filter\", not a snapshot of today's types. Up to 10 named events otherwise; there are 13 event types, so a hook that wants all of them sends `[]`. Duplicates collapse. An unknown name answers 400 `unknown_event` with `meta.unknown` (the names we do not know) and `meta.valid_events` (every valid name); `ping` is not subscribable."
          },
          "payload": {
            "type": "string",
            "enum": [
              "full",
              "minimal"
            ],
            "description": "The body this hook receives. `full` (the default): the documented body — envelope with `workspace_id`, `metadata`, the `signer` block and every event field. `minimal`: identifiers only, no personal data — `event`, `event_id`, `created_at`, `document_id` on every event; `signing_request_id`, `role`, `reference` on signing_request.* events; `signed_pdf_url`, `sha256`, `audit_trail_url` on document.completed — sent with `User-Agent: wesign-webhooks/1` (a full hook keeps `letssign.now-webhooks/1.0`). See the Minimal* schemas. Omitted, a new target_url gets `full`; a re-POST that replaces an ACTIVE `minimal` hook on the same URL stays `minimal` — `payload` is never switched back to `full` by leaving it out; send `\"full\"` to do that. A deployment without support for minimal payloads answers 503 `payload_mode_unavailable` (nothing is created) rather than creating a full hook; production supports them.",
            "default": "full"
          }
        },
        "description": "An unknown top-level key is ignored. A target_url that an ACTIVE workspace-wide hook of this workspace already targets (compared as a parsed URL: scheme and host case and a default port do not matter, the path and a trailing slash do) REPLACES that hook — see POST /hooks."
      },
      "HookSecretRotated": {
        "type": "object",
        "description": "The POST /hooks/{id}/rotate response: the new secret, shown once.",
        "required": [
          "id",
          "secret",
          "secret_prefix",
          "previous_secret_valid_until"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "secret": {
            "type": "string",
            "pattern": "^whsec_[0-9a-f]{48}$",
            "description": "The new signing secret. Shown once, only here — GET /hooks never includes it."
          },
          "secret_prefix": {
            "type": "string",
            "pattern": "^whsec_[0-9a-f]{8}$",
            "description": "First 14 characters of `secret`, for display."
          },
          "previous_secret_valid_until": {
            "type": "string",
            "format": "date-time",
            "description": "Until this instant (24 h after the rotation) deliveries are also signed with the previous secret, as a second `v1=` entry."
          }
        }
      },
      "Signer": {
        "type": "object",
        "required": [
          "email",
          "role"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "role": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_-]{0,40}$",
            "description": "Lowercase ascii role that anchors ([[ls:KIND:ROLE]]) and explicit fields reference. 1–41 chars, leading letter."
          },
          "name": {
            "type": "string",
            "maxLength": 200,
            "description": "Display name, at most 200 characters, printed as sent (put a title such as \"Dr.\" where it should print). Ignored when `first_name` or `last_name` is non-blank: the stored name is then \"{first_name} {last_name}\", a blank half left out. Send one form, not both."
          },
          "first_name": {
            "type": "string",
            "maxLength": 100,
            "description": "Trimmed. If either `first_name` or `last_name` is non-blank, the stored name is \"{first_name} {last_name}\" (a blank half left out) and `name` is ignored. Only the stored name is read back (`name`)."
          },
          "last_name": {
            "type": "string",
            "maxLength": 100,
            "description": "Trimmed. If either `first_name` or `last_name` is non-blank, the stored name is \"{first_name} {last_name}\" (a blank half left out) and `name` is ignored. Only the stored name is read back (`name`)."
          },
          "recipient_color": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Accent for this signer's fields on the sign view (#RRGGBB)."
          },
          "role_label": {
            "description": "Human label for the role, shown on the auto-appended signature page.",
            "oneOf": [
              {
                "type": "string",
                "maxLength": 120
              },
              {
                "type": "object",
                "required": [
                  "en",
                  "de"
                ],
                "properties": {
                  "en": {
                    "type": "string",
                    "maxLength": 120
                  },
                  "de": {
                    "type": "string",
                    "maxLength": 120
                  }
                }
              }
            ]
          },
          "locale": {
            "$ref": "#/components/schemas/Locale"
          },
          "signing_order": {
            "type": "integer",
            "minimum": 1,
            "maximum": 20,
            "description": "Sequential mode only; unset signers are numbered by array position."
          },
          "phone_e164": {
            "type": "string",
            "pattern": "^\\+[1-9]\\d{7,14}$",
            "description": "The signer's mobile number in E.164 (`+`, then 8–15 digits, no spaces), e.g. +41791112233. Required when `require_sms_verification` is true (400 `invalid_request` otherwise). Only the shape is checked here: a landline or undeliverable number shows when the signer asks for a code (webhook `signing_request.sms_failed`). Correct it with PATCH /signing-requests/{id}."
          },
          "require_sms_verification": {
            "type": "boolean",
            "description": "Second factor: the signer must enter a one-time code sent by SMS to `phone_e164` (AES; without it the signature is SES). When the code is asked for is set by `sms_gate`. Available on every tier. Each workspace has a monthly SMS allowance: Free 25, Pro 50, Branded 200, Teams and Enterprise 100 per paid seat, plus any standing raise we set on request (no price per SMS); every code sent counts once, resends included, and the counter resets at 00:00 UTC on the 1st. A create, instantiate or confirm whose SMS signers outnumber the codes left this month is refused with 402 `sms_allowance_exhausted` before anything is created. A request is never downgraded to a signature without SMS. If the allowance runs out after the request was created, the signer cannot get a code until it resets (webhook `signing_request.sms_failed`, `reason: allowance_exhausted`) and cannot sign until then. GET /me reports the allowance as `sms_allowance`."
          },
          "sms_gate": {
            "$ref": "#/components/schemas/SmsGate"
          },
          "reference": {
            "$ref": "#/components/schemas/SignerReference"
          },
          "company": {
            "$ref": "#/components/schemas/SignerCompany"
          },
          "job_title": {
            "$ref": "#/components/schemas/SignerJobTitle"
          }
        },
        "additionalProperties": false,
        "description": "Strict: a key other than the ones below answers 400 `unknown_signer_field` with `meta.signers` = [{ index, fields }] and `meta.accepted` — it is never dropped silently (a dropped key could be the SMS factor)."
      },
      "ExplicitField": {
        "type": "object",
        "required": [
          "page",
          "x",
          "y",
          "w",
          "h",
          "role"
        ],
        "properties": {
          "page": {
            "type": "integer",
            "minimum": 0,
            "description": "0-based page index."
          },
          "x": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Fraction of page width."
          },
          "y": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Fraction of page height."
          },
          "w": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "h": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "kind": {
            "type": "string",
            "enum": [
              "signature",
              "initial",
              "date",
              "text"
            ],
            "default": "signature"
          },
          "role": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_-]{0,40}$",
            "description": "Must match a signer's `role`."
          },
          "origin": {
            "type": "string",
            "enum": [
              "top-left",
              "center"
            ],
            "default": "top-left",
            "description": "Whether x/y is the field's top-left corner or its centre (converted server-side)."
          }
        }
      },
      "ApiMetadata": {
        "type": [
          "object",
          "null"
        ],
        "maxProperties": 16,
        "propertyNames": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_.-]{1,40}$"
        },
        "additionalProperties": {
          "type": [
            "string",
            "number",
            "boolean",
            "null"
          ],
          "maxLength": 500
        },
        "description": "Your own reference for a document (case number, CRM id, tenant…). Flat only: up to 16 keys matching ^[A-Za-z0-9_.-]{1,40}$, values string (≤ 500 characters), finite number, boolean or null — no nested objects or arrays — and at most 4096 bytes as JSON. Stored with the document and echoed back verbatim in the create response, on GET /documents/{id} and on every webhook payload that names the document. null (or an empty object on input) means none. Accepted by POST /signing-requests, POST /templates/{id}/instantiate and POST /templates/{id}/generate with exactly this shape."
      },
      "MetadataProblem": {
        "type": "object",
        "required": [
          "path",
          "message"
        ],
        "properties": {
          "path": {
            "type": "string",
            "description": "`metadata` for a whole-object problem (not an object, too many keys, over 4096 bytes) or `metadata.<key>` for a bad key or value."
          },
          "message": {
            "type": "string"
          }
        }
      },
      "MetadataInvalid": {
        "type": "object",
        "description": "The 400 `invalid_metadata` body, identical on every create endpoint.",
        "required": [
          "error",
          "code",
          "problems"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "`N problem(s) with metadata.`"
          },
          "code": {
            "type": "string",
            "const": "invalid_metadata"
          },
          "problems": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/MetadataProblem"
            },
            "description": "Top-level, not under `meta` — the same placement template input problems use."
          }
        }
      },
      "SigningRequestCreate": {
        "type": "object",
        "required": [
          "signers"
        ],
        "properties": {
          "signers": {
            "type": "array",
            "minItems": 1,
            "maxItems": 20,
            "items": {
              "$ref": "#/components/schemas/Signer"
            }
          },
          "placement": {
            "type": "string",
            "enum": [
              "anchors",
              "auto_append",
              "explicit",
              "manual"
            ],
            "default": "anchors",
            "description": "`manual` is retired and answers 400 `placement_retired`."
          },
          "fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ExplicitField"
            },
            "description": "Required for placement `explicit`; every signer needs at least one signature or initial field."
          },
          "file_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Fetch the PDF from this http(s) URL instead of uploading it. Public hosts only, no redirects, ≤ 25 MB. Required for JSON bodies."
          },
          "filename": {
            "type": "string",
            "maxLength": 200,
            "description": "Stored filename, sanitised: letters and digits of any script, `_`, `.` and `-` are kept (`Müller` stays `Müller`); spaces and any other character become `_`. Overrides the name derived from `file_url` or the uploaded file; honoured on multipart bodies too. Shown to signers and used as the invitation subject — unless the signer has `sms_gate: before_view`."
          },
          "observer_emails": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "type": "string",
              "format": "email",
              "maxLength": 254
            },
            "description": "CC'd on dispatch and completion; de-duplicated and lower-cased."
          },
          "callback_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Registers a document-scoped webhook for this document's events. Its secret comes back once under `callback.secret`. The hook receives the full body; the API cannot make it minimal. For identifiers only, register a workspace hook with POST /hooks and `payload: \"minimal\"`, and do not pass `callback_url`."
          },
          "metadata": {
            "$ref": "#/components/schemas/ApiMetadata",
            "description": "Optional. A malformed value is a 400 `invalid_metadata` (before 2026-09-24 the key was dropped silently, so any value passed). Kept on the document so a webhook can relink it if you lose this call's response. On multipart bodies send it as a JSON-encoded string, like `signers`. Part of the body hashed for `Idempotency-Key`: the same key with different metadata answers 422 idempotency_key_reuse."
          },
          "signing_mode": {
            "type": "string",
            "enum": [
              "parallel",
              "sequential"
            ],
            "default": "parallel"
          },
          "locale": {
            "$ref": "#/components/schemas/Locale"
          },
          "expires_in_days": {
            "type": "integer",
            "minimum": 1,
            "maximum": 90,
            "default": 14
          },
          "placement_assignee_email": {
            "type": "string",
            "format": "email",
            "maxLength": 254,
            "deprecated": true,
            "description": "Belonged to the retired manual placement. Accepted and ignored."
          },
          "send_emails": {
            "type": "boolean",
            "default": true,
            "description": "false: the document, the signing requests, their signing URLs and the `signing_request.sent` webhook are created as usual, but no invitation email and no observer notice go out — `emailed` is false for every signer and you distribute `signingUrl` yourself. In a sequential send that holds for the later signers too: the choice is stored on every signer, so a signer released when their turn comes is not emailed either; the release emits `signing_request.sent` (`source: \"sequential_release\"`, `emailed: false`) as your cue. Signing requests created before the 2026-09-25 release do not carry the choice and are emailed on release. It does not stop: the SMS code (texted when the signer asks for it — at the Sign press with `before_sign`, by tapping \"Text me a code\" on the protected page with `before_view`), the workspace's automatic reminders (7 and 14 days by default, Settings → Signing) or the completion emails. On multipart send `true`/`false`. Any other value (the JSON string \"false\", a multipart \"no\") is a 400 `invalid_request`; before 2026-09-24 the key was ignored. Part of the body hashed for `Idempotency-Key`."
          }
        }
      },
      "SigningRequestCreated": {
        "type": "object",
        "required": [
          "documentId",
          "metadata",
          "status",
          "placement",
          "presentedSha256",
          "anchors",
          "signers",
          "observers",
          "warnings"
        ],
        "properties": {
          "documentId": {
            "type": "string",
            "format": "uuid"
          },
          "metadata": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ApiMetadata"
              },
              {
                "type": "null"
              }
            ],
            "description": "The `metadata` you sent, exactly as stored; null when none was given. Always present."
          },
          "status": {
            "type": "string",
            "const": "pending"
          },
          "placement": {
            "type": "string",
            "enum": [
              "anchors",
              "auto_append",
              "explicit"
            ],
            "description": "The placement actually used (`anchors` with zero matches reports `auto_append`)."
          },
          "presentedSha256": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "SHA-256 of the PDF as presented to signers (markers masked / signature page appended)."
          },
          "anchors": {
            "type": "object",
            "required": [
              "found",
              "fields"
            ],
            "properties": {
              "found": {
                "type": "integer",
                "description": "Anchor markers matched (0 unless placement was `anchors`)."
              },
              "fields": {
                "type": "integer",
                "description": "Fields placed in total."
              }
            }
          },
          "signers": {
            "type": "array",
            "items": {
              "allOf": [
                {
                  "type": "object",
                  "required": [
                    "role",
                    "email",
                    "status",
                    "signingUrl",
                    "signingRequestId",
                    "emailed"
                  ],
                  "properties": {
                    "role": {
                      "type": "string"
                    },
                    "email": {
                      "type": "string",
                      "format": "email"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "queued"
                      ]
                    },
                    "signingUrl": {
                      "type": "string",
                      "format": "uri",
                      "description": "The signer's personal link, the same one the invitation email carries: `https://<slug>.wesign.now/<locale>/sign/<token>` on a branded workspace (Branded, Teams, Enterprise), else `https://www.wesign.now/<locale>/sign/<token>`. Contains their credential."
                    },
                    "signingRequestId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "emailed": {
                      "type": "boolean",
                      "description": "Whether the invitation email went out: false for queued sequential signers, with `send_emails: false`, when the send failed, or when the signer's address is on a domain that accepts no email (a null MX, no such domain, or no mail server — we do not send what can only bounce, and the document's sender is emailed that the address needs correcting) (the URL still works)."
                    }
                  }
                },
                {
                  "$ref": "#/components/schemas/SignerReadback"
                }
              ]
            }
          },
          "observers": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "email"
            }
          },
          "callback": {
            "type": "object",
            "description": "Only when `callback_url` was given.",
            "required": [
              "url",
              "secret",
              "note"
            ],
            "properties": {
              "url": {
                "type": "string",
                "format": "uri"
              },
              "secret": {
                "type": "string",
                "pattern": "^whsec_[0-9a-f]{48}$",
                "description": "Shown once. Verify the webhook signature header with it."
              },
              "note": {
                "type": "string"
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiWarning"
            },
            "description": "Unknown top-level keys (JSON keys, or multipart fields other than `file`) that were ignored. Empty when there were none."
          }
        }
      },
      "SigningRequest": {
        "type": "object",
        "required": [
          "id",
          "documentId",
          "signer",
          "status",
          "locale",
          "channel",
          "expiresAt",
          "createdAt",
          "staged",
          "signingUrl",
          "auditEvents"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "documentId": {
            "type": "string",
            "format": "uuid"
          },
          "signer": {
            "allOf": [
              {
                "type": "object",
                "required": [
                  "email",
                  "name",
                  "role",
                  "order"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "role": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "order": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "Signing order in sequential mode; null otherwise."
                  }
                }
              },
              {
                "$ref": "#/components/schemas/SignerReadback"
              }
            ]
          },
          "status": {
            "$ref": "#/components/schemas/SigningRequestStatus"
          },
          "locale": {
            "type": [
              "string",
              "null"
            ]
          },
          "channel": {
            "type": [
              "string",
              "null"
            ],
            "description": "email | sms | both."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "staged": {
            "type": "boolean",
            "description": "True while the row belongs to a staged template instance (`review: true`, not confirmed yet): `status` reads `queued` and nothing has been sent."
          },
          "signingUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "The signer's personal link — contains their credential. Null while `staged`: until a confirm releases it the token opens a waiting notice, never the document, and signing with it is refused."
          },
          "auditEvents": {
            "type": "array",
            "maxItems": 50,
            "description": "The FIRST 50 audit events of this request, oldest first — a request with more than 50 shows its earliest 50, not its latest. `phone_changed` and `sms_verify_sent` name the phone masked; the invitation-SMS rows `sms_sent` / `sms_failed` (confirm with `channel` sms or both) carry the full number in `meta.to`.",
            "items": {
              "$ref": "#/components/schemas/AuditEvent"
            }
          }
        }
      },
      "AuditEvent": {
        "type": "object",
        "required": [
          "type",
          "createdAt",
          "meta"
        ],
        "properties": {
          "type": {
            "type": "string",
            "description": "e.g. `email_sent`, `queued`, `viewed` (the signer's browser first loaded the document — written from the 2026-09-24 release on; `meta` null), `reminder_sent`, `withdrawn`, `declined` (`meta.reason`, the full text; `meta` null when no reason was given), `signed`, `sms_verify_sent` (a verification code went out; `meta.masked_phone`, `meta.quota_used`, `meta.quota_cap`), `sms_verify_failed` (`meta.phase` send|check, `meta.reason`), `sms_verify_ok`, `phone_changed` (`meta.from` / `meta.to`, masked; `meta.source`, `meta.api_key_id`), `sms_sent` / `sms_failed` (the invitation SMS of confirm `channel` sms/both — not the code; `meta.to` is the full number), `observer_notified` (the notice to the observers, on the first invited signer's request; `meta.observers`), `observer_completed` (the observers' completion email, on the completing signer's request; `meta.observers`), `api_instantiated`, `instance_staged`, `instance_confirmed`. The masked IP address and user agent of a view, a decline or a signature are kept for the audit certificate (GET /documents/{id}/audit-trail), not returned in `meta`. New types may appear."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "meta": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true
          }
        }
      },
      "Document": {
        "type": "object",
        "required": [
          "id",
          "filename",
          "sha256",
          "observers",
          "callbackConfigured",
          "metadata",
          "createdAt",
          "status",
          "signers",
          "review",
          "placement"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "filename": {
            "type": "string"
          },
          "sha256": {
            "type": "string",
            "description": "SHA-256 of the PDF as it was sent for signature — not of the signed PDF, whose hash is `sha256` on `document.completed` (`pending` for a template instance whose render is not yet hashed)."
          },
          "observers": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "email"
            }
          },
          "callbackConfigured": {
            "type": "boolean",
            "description": "Whether a `callback_url` webhook is registered for this document."
          },
          "metadata": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ApiMetadata"
              },
              {
                "type": "null"
              }
            ],
            "description": "The `metadata` given when the document was created through the API; null for documents created without one (including every dashboard upload)."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "staged",
              "awaiting_placement",
              "pending",
              "partial",
              "signed",
              "expired",
              "withdrawn"
            ],
            "description": "Aggregate over the signer rows: `staged` (template instance awaiting review), `signed` (every signer row is `signed` — the same condition that fires `document.completed` and lets `/signed` + `/audit-trail` serve), `partial` (some signed — including a document whose other signer declined, was withdrawn or expired; such a document never reaches `signed`), `expired` / `withdrawn` (all), else `pending`. `awaiting_placement` is a legacy state from the retired manual flow."
          },
          "signers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DocumentSigner"
            }
          },
          "review": {
            "description": "Present only while the document is a staged template instance.",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "required": [
                  "kind",
                  "staged_at",
                  "expires_at",
                  "url"
                ],
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "esign",
                      "file"
                    ]
                  },
                  "staged_at": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time"
                  },
                  "expires_at": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time",
                    "description": "End of the 14-day review window."
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "The review page in the web app (locale `en`), for a signed-in member of the workspace; see InstantiateStaged.review_url."
                  }
                }
              }
            ]
          },
          "placement": {
            "description": "Legacy: a still-open manual placement (the flow is retired; always null for documents created today).",
            "oneOf": [
              {
                "type": "null"
              },
              {
                "type": "object",
                "required": [
                  "assigneeEmail",
                  "expiresAt",
                  "placementUrl"
                ],
                "properties": {
                  "assigneeEmail": {
                    "type": "string"
                  },
                  "expiresAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "placementUrl": {
                    "type": "string",
                    "format": "uri"
                  }
                }
              }
            ]
          }
        }
      },
      "DocumentSigner": {
        "type": "object",
        "required": [
          "id",
          "email",
          "name",
          "role",
          "company",
          "job_title",
          "order",
          "status",
          "locale",
          "expiresAt",
          "staged",
          "signingUrl"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The signing request id."
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "role": {
            "type": [
              "string",
              "null"
            ]
          },
          "company": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "The company you stated for this signer; null when none was given. Never verified."
          },
          "job_title": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120,
            "description": "The function you stated for this signer; null when none was given. Never verified."
          },
          "order": {
            "type": [
              "integer",
              "null"
            ]
          },
          "status": {
            "$ref": "#/components/schemas/SigningRequestStatus"
          },
          "locale": {
            "type": [
              "string",
              "null"
            ]
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "staged": {
            "type": "boolean",
            "description": "True while the row is a staged review row (not sent yet)."
          },
          "signingUrl": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Null while staged — the link would be dead."
          }
        }
      },
      "Template": {
        "type": "object",
        "required": [
          "id",
          "name",
          "description",
          "status",
          "version",
          "pages",
          "content_kind",
          "instantiable",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "locked",
              "archived"
            ],
            "description": "`draft` (still being edited), `locked` (published: the only status `/instantiate` and `/generate` accept; any other answers 409 `template_unlocked`) or `archived` (deleted in the app). GET /templates never lists an archived template; GET /templates/{id} still answers for one, with `instantiable: false`."
          },
          "version": {
            "type": "integer"
          },
          "pages": {
            "type": [
              "integer",
              "null"
            ]
          },
          "content_kind": {
            "type": [
              "string",
              "null"
            ],
            "description": "`richtext` for a template authored in the editor (`{{placeholders}}`, tables, conditional sections), `pdf` for one built on an uploaded PDF (its inputs are positioned text boxes, each with a `slot`)."
          },
          "instantiable": {
            "type": "boolean",
            "description": "`status === 'locked'`."
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TemplateInputField": {
        "type": "object",
        "required": [
          "key",
          "kind",
          "owner",
          "required",
          "standard"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "The `field_values` key — the FieldDefinition.key rule GET /fields enforces: lowercase segments `[a-z][a-z0-9_]*` joined by single dots, at most 64 characters (`client_name`, `person.date_of_birth`). A per-signer token of a multi-signer template carries a `_s2`, `_s3`… suffix on top (`recipient_name_s2`), which may take it past 64 characters."
          },
          "kind": {
            "type": "string",
            "enum": [
              "scalar",
              "collection"
            ]
          },
          "owner": {
            "type": "string",
            "enum": [
              "sender",
              "signer",
              "api"
            ],
            "description": "Who supplies the value: `api` (your integration), `sender` (the person sending; your integration stands in for them) or `signer` (the recipient, while signing). Where it comes from: a rich-text `{{placeholder}}` carries its author's choice; a collection is `api` when `mode` is `api`, else `sender`; a PDF template's positioned text box (`slot` set) follows the author's \"Filled by\" choice: `sender` (Sender / API) or `signer`, never `api`. A `sender` box is fixed document content: the value you send (else the author's default) prints as text the signer cannot change, every signer of the document sees it from their first view whichever slot the box sits on, and it is sealed once. A `signer` box is typed by its signer; you may prefill it. Whether you must send a value is `required`, never derived from `owner`: signer-owned inputs are never API-required, and a `sender` or `api` input is required unless `required` says otherwise."
          },
          "required": {
            "type": "boolean",
            "description": "TRUE means the API caller must supply this: the whole contract. Read it, never re-derive it from `owner`. It is computed once from the template: a `sender` or `api` placeholder is required unless it is a platform token (`auto_filled`) or the author made it optional; a collection is required exactly when `mode` is `api`; a positioned `sender` box is required unless the author made it optional or gave it a default (`has_default`: the default then prints when you send nothing); a `signer` input, placeholder or box, never is. An input inside a conditional section is demanded only while your values show that section; a `validate_only` dry run applies the conditions. `/generate` never demands a positioned field (`slot` set), whatever this says: it does not print them."
          },
          "auto_filled": {
            "type": "boolean",
            "const": true,
            "description": "Platform token (date_today, sender_*, recipient_*) — filled automatically, never required of a caller."
          },
          "label": {
            "type": "string",
            "description": "Scalars: the name to show your user — the workspace registry's label, else the template author's, else, for a standard key, the standard name in the document's language (English when that is unknown), else the key made readable. Prefer `labels` in your user's language when it has one."
          },
          "type": {
            "type": "string",
            "description": "Data type: a FieldDefinition.type value, or a built-in field's own type (`iban` on signer_iban, `vat` on signer_vat_id, `country` on signer_country); a standard key (see `standard`) that neither the template nor the registry types takes the standard type (`date` for `person.date_of_birth`, `email` for `contact.email`, `phone` for `contact.phone`, `country` for `address.country`); an entry nothing types reads `text`. For `boolean` / `agree` send a yes/no STRING (`\"true\"`, `\"false\"`, `\"yes\"`, `\"no\"` or a UI-locale label such as `\"ja\"`/`\"nein\"`), case-insensitive; anything else is 422 template_input_invalid with `invalid_boolean` / `invalid_agree` — never silently left empty. `agree` accepts only the affirmative spellings. A `date` takes a day-first date (`DD.MM.YYYY`, `DD/MM/YYYY`, `DD-MM-YYYY`, `DD MM YYYY`) or `YYYY-MM-DD`, stored as `YYYY-MM-DD` — see FieldValues. A `time` takes `HH:MM` or `HH.MM` (24-hour, `14:30`, `9:05`) or `h:MM AM/PM` (`2:30 PM`, `2:30pm`) and stores it as 24-hour `HH:MM`; anything else is 422 `invalid_time`."
          },
          "source": {
            "type": "string",
            "enum": [
              "lead",
              "api"
            ],
            "description": "LEGACY: kept so existing clients keep parsing, and it says nothing about who fills the field or whether you must send it. Read `owner` and `required` instead. The value is the workspace field registry's `source` for the key (`lead` = meant for the Present lead form, `api` = meant for the API; see GET /fields), `lead` when the registry has no entry for the key, and always `api` on a collection. A Sender / API box whose key is not in the registry therefore reads `lead`. No removal is scheduled."
          },
          "example": {
            "type": "string",
            "description": "A sample value in the stored spelling (dates `YYYY-MM-DD`): the workspace registry's example, else the standard catalogue's for a standard key. One exception: the standard `contact.phone` example is E.164 (`+41791234567`), what you send; the API stores and echoes a recognised number grouped (`+41 79 123 45 67`), and its `pattern` accepts both."
          },
          "max_length": {
            "type": "integer"
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Enum display labels."
          },
          "option_keys": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Enum raw keys; the validator accepts either spelling."
          },
          "slot": {
            "type": "integer",
            "description": "Positioned fields (a PDF template's text boxes) only; absent on placeholders and collections. On a `signer` box: the recipient slot whose signer fills it. On a `sender` box: the slot whose signing request STORES the value, not who sees it. Every signer of the document sees every Sender / API value as fixed content from their first view, in any signing order, and each value is sealed exactly once (since the 2026-09-24 release). Every slot that has a positioned field needs a recipient (400 `missing_recipients`)."
          },
          "signer_required": {
            "type": "boolean",
            "description": "Whether the SIGNER must fill it: informational, never an API requirement. Present on a `signer` rich-text placeholder (the author's Required switch, which the signing page enforces) and on every positioned text field, where it is always false: an empty text field never blocks signing. Absent on `sender` / `api` placeholders and on collections."
          },
          "has_default": {
            "type": "boolean",
            "description": "Positioned text fields only (always present there, true or false; absent elsewhere): the author set a default value. When you send nothing it prints (a `sender` box, which is then not `required`) or prefills the box (a `signer` box). An explicit empty string wins over the default and leaves the box empty."
          },
          "mode": {
            "type": "string",
            "enum": [
              "api",
              "authored",
              "interactive"
            ],
            "description": "Collections: `api` = caller must supply rows; `authored` = sender defaults render when omitted; `interactive` = the recipient configures the rows at signing unless the API fixes them."
          },
          "columns": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "key",
                "label"
              ],
              "properties": {
                "key": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                }
              }
            }
          },
          "date_format": {
            "type": "string",
            "enum": [
              "dmy_dot",
              "dmy_slash",
              "mdy_slash",
              "long"
            ],
            "readOnly": true,
            "description": "READ-ONLY: how this date PRINTS in the document, as the template's author picked it — `dmy_dot` 15.03.1990, `dmy_slash` 15/03/1990, `mdy_slash` 03/15/1990, `long` written out in `lang` (24. September 2026). Absent = not picked: a date prints 15.03.1990 and `date_today` is written out. Never an input rule: send a date as described in FieldValues whatever this says; it is stored and echoed as `YYYY-MM-DD`."
          },
          "time_format": {
            "type": "string",
            "enum": [
              "h24",
              "h12"
            ],
            "readOnly": true,
            "description": "READ-ONLY: how this time PRINTS — `h24` 14:30, `h12` 2:30 PM. Absent = 24-hour. Send `14:30`, `14.30` or `2:30 PM` whatever this says; it is stored and echoed as `HH:MM`."
          },
          "lang": {
            "type": "string",
            "enum": [
              "en",
              "de",
              "es",
              "fr",
              "it",
              "nl"
            ],
            "readOnly": true,
            "description": "READ-ONLY: the language a `long` date is written out in. The editor records the author's language with every date or time placeholder, so it can also appear on a numeric date or a time, where it changes nothing. Absent = the document's language (the language its signature lines were written in), else the call's `locale` (instantiate, and generate without review — a staged review renders without one so that it matches its later confirm), else English."
          },
          "standard": {
            "type": "boolean",
            "description": "True when `key` (or its per-signer base: `signing.place_s2` → `signing.place`) is one of the platform's standard field names — English dotted keys anchored to international vocabularies (schema.org, ISO 3166-1, E.164, ETSI EN 319 142 / PAdES, the Swiss UID and AHV registers): `person.first_name`, `person.last_name`, `person.full_name`, `person.date_of_birth`, `person.tax_id`, `person.ch_ahv_number`, `company.legal_name`, `company.uid`, `company.tax_id`, `company.vat_id`, `address.street`, `address.postal_code`, `address.city`, `address.country`, `address.full`, `contact.email`, `contact.phone`, `signing.place`, `signing.date`. Map your data to these once and it fills every template that uses them. A SUGGESTION only: a custom key (`standard: false`) is just as valid and never refused. Always present."
          },
          "labels": {
            "$ref": "#/components/schemas/FieldLabels"
          },
          "pattern": {
            "type": "string",
            "description": "Standard keys only: a regular expression (ECMA-262, anchored, `u`-flag safe) of what a value usually looks like — e.g. `^CHE-?\\d{3}\\.?\\d{3}\\.?\\d{3}$` for `company.uid`. A HINT for your own form: the API never enforces it (only `type` is validated)."
          },
          "description": {
            "type": "string",
            "description": "Standard keys only: one English sentence saying what the value is."
          }
        }
      },
      "FieldLabels": {
        "type": "object",
        "properties": {
          "en": {
            "type": "string"
          },
          "de": {
            "type": "string"
          },
          "es": {
            "type": "string"
          },
          "fr": {
            "type": "string"
          },
          "it": {
            "type": "string"
          },
          "nl": {
            "type": "string"
          }
        },
        "additionalProperties": false,
        "description": "The field's name in the UI languages, for asking your user. A standard key carries all six (the platform's translations). A custom key carries at most the template's own label, under the language the template records it is written in (rich-text templates record it on their signature lines; PDF templates do not) — never a machine translation. Absent = unknown; fall back to `label`."
      },
      "TemplateSigningField": {
        "type": "object",
        "required": [
          "kind",
          "slot",
          "required"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "signature",
              "initial",
              "date"
            ]
          },
          "slot": {
            "type": "integer"
          },
          "required": {
            "type": "boolean"
          },
          "date_format": {
            "type": "string",
            "enum": [
              "dmy_dot",
              "dmy_slash",
              "mdy_slash",
              "long"
            ],
            "readOnly": true,
            "description": "`date` fields only, READ-ONLY: how the day this signer signs is stamped. Absent = the workspace's house format (a US workspace 03/15/1990, every other 15.03.1990)."
          },
          "lang": {
            "type": "string",
            "enum": [
              "en",
              "de",
              "es",
              "fr",
              "it",
              "nl"
            ],
            "readOnly": true,
            "description": "`date` fields only, READ-ONLY, and only next to a `date_format`: the language a `long` date is written out in (a numeric format ignores it). Absent = the signing request's `locale`."
          }
        }
      },
      "TemplateDetail": {
        "type": "object",
        "required": [
          "template",
          "recipients",
          "field_values",
          "signing_fields"
        ],
        "properties": {
          "template": {
            "$ref": "#/components/schemas/Template"
          },
          "recipients": {
            "type": "array",
            "description": "The recipient slots the template's fields use, ascending — send one instantiate recipient per slot. Each carries the author's `role` and identity pin when set; an unnamed, unpinned slot is exactly `{ slot }`. Roles and pins are the template's current ones, also when you instantiate a pinned `version` (they are enforced live).",
            "items": {
              "type": "object",
              "required": [
                "slot"
              ],
              "properties": {
                "slot": {
                  "type": "integer"
                },
                "role": {
                  "type": "string",
                  "description": "The author's name for the slot (\"Landlord\", \"Client\"). Absent when the author named none. Informational: it labels the invitation and signing page; you never send it."
                },
                "pinned": {
                  "type": "string",
                  "enum": [
                    "sender",
                    "email"
                  ],
                  "description": "Absent = anyone. `email`: the slot is fixed to `pinned_email`; instantiate and confirm answer 400 `pinned-slot-mismatch` (with `slot`) for any other address, so send exactly that one. `sender`: the slot is whoever sends — the signed-in sender in the app (quick-send, a form link's creator). An API key has no signed-in sender, so instantiate does not enforce it: send the address your workflow needs."
                },
                "pinned_email": {
                  "type": "string",
                  "format": "email",
                  "description": "`pinned: \"email\"` only: the address the slot is fixed to, lowercased. Served only to the template's own workspace — the same audience the 400 `pinned-slot-mismatch` message already names it to."
                }
              },
              "additionalProperties": false
            }
          },
          "field_values": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TemplateInputField"
            }
          },
          "signing_fields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TemplateSigningField"
            }
          }
        }
      },
      "FieldValues": {
        "type": "object",
        "description": "Keyed by input key — the `key` of GET /templates/{id} → `field_values[]`: lowercase segments `[a-z][a-z0-9_]*` joined by single dots, at most 64 characters (`client_name`, `person.date_of_birth`), plus a per-signer `_s2`… suffix where the template has one. The map is FLAT: a dotted key is one key (`{\"person.date_of_birth\": \"15.03.1990\"}`), never a path — a nested object such as `{\"person\": {…}}` fails the body schema (400 invalid_request). A key the template does not know is reported in `warnings[]` (`unknown_field`) and never rendered — the value is kept with the document and echoed in `field_values_echoed`, it is not refused. A string for scalars, or an array of row objects (string cells) for a collection. Every scalar is a STRING. `boolean` fields take `\"true\"`/`\"false\"`, `\"yes\"`/`\"no\"` or a UI-locale label (`\"ja\"`, `\"nein\"`, `\"sí\"`, `\"si\"`, `\"oui\"`, `\"non\"`, `\"sì\"`, `\"nee\"`), trimmed, case-insensitive; `true`/`yes` are stored and rendered as `Yes`, `false`/`no` as `No`, a locale label as sent (trimmed). Any other value is 422 template_input_invalid (`invalid_boolean`), never silently left empty; an `agree` field accepts only a yes spelling. `date` fields take a real calendar day, written DAY FIRST — `DD.MM.YYYY`, `DD/MM/YYYY`, `DD-MM-YYYY` or `DD MM YYYY`, day and month one or two digits (`15.03.1990`, `5.3.1990`, `15/03/1990`, `15 03 1990`) — or as `YYYY-MM-DD`; the value is stored and echoed as `YYYY-MM-DD` (`15.03.1990` → `1990-03-15`) and printed in the format the template picked for that placeholder (TemplateInputField.date_format; `15.03.1990` when none is picked). Day first always: `03/04/1990` is 3 April, and a month-first `03/15/1990` is `invalid_date`, never reinterpreted — as are an impossible day (`2026-02-30`, `31.04.2026`), a two-digit year and a month name. `time` fields take `HH:MM` or `HH.MM` (24-hour: `14:30`, `9:05`) or `h:MM AM/PM` (`2:30 PM`, `2:30pm`, `2.30 PM`); stored and echoed as 24-hour `HH:MM`, printed as the template picked (`14:30` unless `2:30 PM` is chosen). `24:00`, `13:00 PM`, `2 PM`, seconds (`14:30:00`), `14h30` and a bare `1430` are `invalid_time`. A raw JSON `true`/`false` or a number does not pass the body schema (400 invalid_request). On a PDF template, a value for a positioned text field (TemplateInputField `slot` set) whose `owner` is `sender` prints as fixed document content the signer cannot change; for a `signer` one it prefills the box the signer may edit. Either falls back to the author's default value when you send nothing.",
        "additionalProperties": {
          "oneOf": [
            {
              "type": "string"
            },
            {
              "type": "array",
              "items": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              }
            }
          ]
        }
      },
      "InputProblem": {
        "type": "object",
        "required": [
          "field",
          "label",
          "code",
          "message"
        ],
        "properties": {
          "field": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "field_required",
              "invalid_shape",
              "invalid_boolean",
              "invalid_agree",
              "invalid_enum",
              "invalid_email",
              "invalid_number",
              "invalid_date",
              "invalid_time",
              "invalid_iban",
              "invalid_vat"
            ]
          },
          "message": {
            "type": "string"
          },
          "allowed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Enum problems: the accepted values."
          }
        }
      },
      "InputWarning": {
        "type": "object",
        "required": [
          "field",
          "code",
          "message"
        ],
        "properties": {
          "field": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "unknown_field",
              "unknown_column"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "TemplateInputInvalid": {
        "type": "object",
        "required": [
          "error",
          "code",
          "template_id",
          "template_name",
          "template_version",
          "problems",
          "warnings",
          "docs"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "`N problems with this request.`"
          },
          "code": {
            "type": "string",
            "const": "template_input_invalid"
          },
          "template_id": {
            "type": "string",
            "format": "uuid"
          },
          "template_name": {
            "type": "string"
          },
          "template_version": {
            "type": [
              "integer",
              "null"
            ]
          },
          "problems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InputProblem"
            },
            "description": "Block the request."
          },
          "warnings": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/ApiWarning"
                },
                {
                  "$ref": "#/components/schemas/InputWarning"
                }
              ]
            },
            "description": "Never block; the value is kept and merely reported. Also lists unknown top-level keys (ApiWarning)."
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "A link for a PERSON, not for your code: the web app's Settings → API page in the request's `locale` (`<app origin>/<locale>/settings/api`; the app origin is letssign.now today, not the API host), where the workspace keeps its field registry and API keys. It needs a session in the app (without one it redirects to the login page) and shows the signed-in member's own workspace. The machine-readable contract is GET /templates/{id}; step by step: https://www.wesign.now/docs/prepare-a-fill."
          },
          "missing_optional": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MissingOptionalField"
            },
            "description": "`validate_only` dry runs only (instantiate and generate): what you may still supply besides fixing `problems`, so one call tells your app what is missing AND what it could still ask for. Absent on a real call's 422."
          }
        }
      },
      "ValidateOnlyResult": {
        "type": "object",
        "required": [
          "ok",
          "template_id",
          "template_version",
          "warnings",
          "missing_optional"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "description": "False exactly when `problems` is non-empty (instantiate only). `/generate` always answers true here — its failures are 4xx."
          },
          "template_id": {
            "type": "string",
            "format": "uuid"
          },
          "template_version": {
            "type": [
              "integer",
              "null"
            ]
          },
          "warnings": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/ApiWarning"
                },
                {
                  "$ref": "#/components/schemas/InputWarning"
                }
              ]
            },
            "description": "Unknown top-level keys and the validator's `field_values` warnings."
          },
          "problems": {
            "type": "array",
            "description": "Instantiate only (always present there, empty when the real call would pass): what the real call would refuse that is not a body error — `tier_required` (the monthly document cap; reported with `review: true` too) and `sms_allowance_exhausted` (not reported with `review: true`: a staged instance is checked when it is confirmed). The real call would answer 402.",
            "items": {
              "oneOf": [
                {
                  "type": "object",
                  "required": [
                    "code",
                    "message",
                    "meta"
                  ],
                  "properties": {
                    "code": {
                      "type": "string",
                      "const": "tier_required"
                    },
                    "message": {
                      "type": "string"
                    },
                    "meta": {
                      "type": "object",
                      "required": [
                        "tier",
                        "cap",
                        "used"
                      ],
                      "properties": {
                        "tier": {
                          "type": "string"
                        },
                        "cap": {
                          "type": "integer"
                        },
                        "used": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                },
                {
                  "type": "object",
                  "required": [
                    "field",
                    "code",
                    "message",
                    "sms_allowance",
                    "required"
                  ],
                  "properties": {
                    "field": {
                      "type": "string",
                      "const": "recipients"
                    },
                    "code": {
                      "type": "string",
                      "const": "sms_allowance_exhausted"
                    },
                    "message": {
                      "type": "string"
                    },
                    "sms_allowance": {
                      "$ref": "#/components/schemas/SmsAllowance"
                    },
                    "required": {
                      "type": "integer",
                      "minimum": 1
                    }
                  }
                }
              ]
            }
          },
          "missing_optional": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MissingOptionalField"
            },
            "description": "The inputs you may still supply and left empty, in template order — what to ask your user for next. Never makes `ok` false. Empty when there is nothing left to offer."
          }
        }
      },
      "MissingOptionalField": {
        "type": "object",
        "description": "An input you MAY still supply and left empty in this request: a signer-owned field you can prefill (otherwise the signer is asked, or it stays empty) or a sender/api input the author made optional. Ask your user for these before the real call; none of them blocks it. Required inputs never appear here (they are `problems`), nor do platform tokens, collections or fields inside a conditional section that is currently hidden. `/generate` never lists positioned fields (it does not print them).",
        "required": [
          "key",
          "label",
          "owner",
          "type",
          "standard"
        ],
        "properties": {
          "key": {
            "type": "string"
          },
          "label": {
            "type": "string",
            "description": "As TemplateInputField.label."
          },
          "owner": {
            "type": "string",
            "enum": [
              "sender",
              "signer",
              "api"
            ]
          },
          "type": {
            "type": "string",
            "description": "As TemplateInputField.type."
          },
          "standard": {
            "type": "boolean",
            "description": "As TemplateInputField.standard."
          },
          "example": {
            "type": "string"
          },
          "pattern": {
            "type": "string",
            "description": "A hint, never enforced — see TemplateInputField.pattern."
          },
          "labels": {
            "$ref": "#/components/schemas/FieldLabels"
          },
          "slot": {
            "type": "integer",
            "description": "Positioned fields only, as TemplateInputField.slot (for a `sender` box, which signing request stores it; every signer sees it)."
          },
          "has_default": {
            "type": "boolean",
            "const": true,
            "description": "The template's own default fills it when you send nothing."
          },
          "date_format": {
            "type": "string",
            "enum": [
              "dmy_dot",
              "dmy_slash",
              "mdy_slash",
              "long"
            ],
            "readOnly": true,
            "description": "As TemplateInputField.date_format (read-only print format; never an input rule)."
          },
          "time_format": {
            "type": "string",
            "enum": [
              "h24",
              "h12"
            ],
            "readOnly": true,
            "description": "As TemplateInputField.time_format (read-only print format; never an input rule)."
          },
          "lang": {
            "type": "string",
            "enum": [
              "en",
              "de",
              "es",
              "fr",
              "it",
              "nl"
            ],
            "readOnly": true,
            "description": "As TemplateInputField.lang (read-only print format; never an input rule)."
          }
        }
      },
      "GenerateBody": {
        "type": "object",
        "properties": {
          "field_values": {
            "$ref": "#/components/schemas/FieldValues"
          },
          "metadata": {
            "$ref": "#/components/schemas/ApiMetadata",
            "description": "Validated in every mode (400 `invalid_metadata`), but stored only when `review: true` creates a document — the streamed default has no document to carry it."
          },
          "filename": {
            "type": "string",
            "maxLength": 200,
            "description": "Output filename, sanitised like POST /signing-requests `filename` (letters and digits of any script kept), `.pdf` appended. Defaults to the template name."
          },
          "review": {
            "type": "boolean",
            "default": false,
            "description": "Stage the result as a file-only document for human review instead of streaming the bytes."
          },
          "validate_only": {
            "type": "boolean",
            "default": false,
            "description": "Dry run: validate `field_values` and return, with `missing_optional` (the inputs you may still supply and left empty; positioned fields are never listed — generate does not print them); no render, no storage."
          },
          "locale": {
            "$ref": "#/components/schemas/Locale"
          }
        }
      },
      "GenerateStaged": {
        "type": "object",
        "required": [
          "document_id",
          "metadata",
          "template_id",
          "template_version",
          "status",
          "kind",
          "filename",
          "review_url",
          "review_expires_at",
          "warnings"
        ],
        "properties": {
          "document_id": {
            "type": "string",
            "format": "uuid"
          },
          "metadata": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ApiMetadata"
              },
              {
                "type": "null"
              }
            ],
            "description": "The `metadata` sent on the request, or null."
          },
          "template_id": {
            "type": "string",
            "format": "uuid"
          },
          "template_version": {
            "type": [
              "integer",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "const": "staged"
          },
          "kind": {
            "type": "string",
            "const": "file"
          },
          "filename": {
            "type": "string"
          },
          "review_url": {
            "type": "string",
            "format": "uri",
            "description": "The review page in the web app, as InstantiateStaged.review_url: a signed-in member of your workspace reads the rendered file there and confirms or discards it."
          },
          "review_expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "warnings": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/ApiWarning"
                },
                {
                  "$ref": "#/components/schemas/InputWarning"
                }
              ]
            },
            "description": "Unknown top-level keys and the validator's `field_values` warnings."
          }
        }
      },
      "InstantiateBody": {
        "type": "object",
        "required": [
          "recipients"
        ],
        "properties": {
          "recipients": {
            "type": "array",
            "minItems": 1,
            "maxItems": 20,
            "description": "One entry per recipient slot the template defines (every slot that has a field must be covered).",
            "items": {
              "type": "object",
              "required": [
                "slot",
                "email"
              ],
              "properties": {
                "slot": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 20
                },
                "email": {
                  "type": "string",
                  "format": "email"
                },
                "name": {
                  "type": "string"
                },
                "phone_e164": {
                  "type": "string",
                  "pattern": "^\\+[1-9]\\d{7,14}$",
                  "description": "E.164, e.g. +41791112233. Required when `require_sms_verification` is true — exactly as on Signer."
                },
                "require_sms_verification": {
                  "type": "boolean",
                  "description": "Second factor: the signer must enter a one-time code sent by SMS (AES), exactly as on POST /signing-requests. Kept on a staged instance: the review page shows it and confirm sends with it. Available on every tier. Each workspace has a monthly SMS allowance: Free 25, Pro 50, Branded 200, Teams and Enterprise 100 per paid seat, plus any standing raise we set on request (no price per SMS); every code sent counts once, resends included, and the counter resets at 00:00 UTC on the 1st. A create, instantiate or confirm whose SMS signers outnumber the codes left this month is refused with 402 `sms_allowance_exhausted` before anything is created. A request is never downgraded to a signature without SMS. If the allowance runs out after the request was created, the signer cannot get a code until it resets (webhook `signing_request.sms_failed`, `reason: allowance_exhausted`) and cannot sign until then. GET /me reports the allowance as `sms_allowance`. A `review: true` instance is checked when it is confirmed."
                },
                "sms_gate": {
                  "$ref": "#/components/schemas/SmsGate"
                },
                "reference": {
                  "$ref": "#/components/schemas/SignerReference"
                },
                "company": {
                  "$ref": "#/components/schemas/SignerCompany"
                },
                "job_title": {
                  "$ref": "#/components/schemas/SignerJobTitle"
                }
              },
              "additionalProperties": false,
              "description": "Strict: a key other than slot, email, name, phone_e164, require_sms_verification, sms_gate, reference, company, job_title answers 400 `unknown_recipient_field` (it is never dropped silently). The SMS fields, `reference`, `company` and `job_title` are spelled exactly as on Signer."
            }
          },
          "metadata": {
            "$ref": "#/components/schemas/ApiMetadata",
            "description": "Stored on the created document and echoed back as `metadata`. Rejected with 400 `invalid_metadata` when malformed. Part of the body hashed for `Idempotency-Key`."
          },
          "field_values": {
            "$ref": "#/components/schemas/FieldValues"
          },
          "locale": {
            "$ref": "#/components/schemas/Locale"
          },
          "expires_in_days": {
            "type": "integer",
            "minimum": 1,
            "maximum": 180,
            "default": 14
          },
          "signing_mode": {
            "type": "string",
            "enum": [
              "parallel",
              "sequential"
            ],
            "default": "parallel",
            "description": "Sequential dispatch follows slot order."
          },
          "version": {
            "type": "integer",
            "minimum": 1,
            "description": "Pin to a specific template version snapshot; defaults to the current one."
          },
          "send_emails": {
            "type": "boolean",
            "default": true,
            "description": "False = no invitation email — not at the send, and not when a sequential signer's turn comes (the release emits `signing_request.sent` with `source: \"sequential_release\"`, `emailed: false`); we return the signing URLs and you distribute them. Does not stop the SMS code, automatic reminders or completion emails (see SigningRequestCreate.send_emails)."
          },
          "review": {
            "type": "boolean",
            "default": false,
            "description": "Stage for human review instead of sending. `send_emails` is remembered and applied at confirm."
          },
          "validate_only": {
            "type": "boolean",
            "default": false,
            "description": "Dry run: runs every check up to and including the input validator — body schema (strict recipients, the SMS and `sms_gate` rules), `metadata`, template found and locked, `version`, every slot with fields covered, each slot has a signature field, slot pins, `field_values` against the input schema, the interactive-table rule — and reports the SMS allowance under `problems`. The answer also lists `missing_optional`: the inputs you may still supply and left empty (on the 200 and on the dry-run 422). Creates nothing, never touches an `Idempotency-Key`. It sends nothing, so it cannot tell whether an email or SMS will be delivered. There is no monthly document cap to check: instantiate applies none, neither on the dry run nor on the real call."
          }
        }
      },
      "InstantiateResult": {
        "type": "object",
        "required": [
          "document_id",
          "metadata",
          "template_version",
          "signing_mode",
          "recipients",
          "field_values_echoed",
          "warnings"
        ],
        "properties": {
          "document_id": {
            "type": "string",
            "format": "uuid"
          },
          "metadata": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ApiMetadata"
              },
              {
                "type": "null"
              }
            ],
            "description": "The `metadata` sent on the request, or null."
          },
          "template_version": {
            "type": "integer"
          },
          "signing_mode": {
            "type": "string",
            "enum": [
              "parallel",
              "sequential"
            ]
          },
          "recipients": {
            "type": "array",
            "items": {
              "allOf": [
                {
                  "type": "object",
                  "required": [
                    "slot",
                    "email",
                    "signing_request_id",
                    "signing_url",
                    "emailed"
                  ],
                  "properties": {
                    "slot": {
                      "type": "integer"
                    },
                    "email": {
                      "type": "string",
                      "format": "email"
                    },
                    "signing_request_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "signing_url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "emailed": {
                      "type": "boolean",
                      "description": "Whether the invite email was sent (false for queued sequential signers, `send_emails: false`, a failed send, or when the signer's address is on a domain that accepts no email (a null MX, no such domain, or no mail server — we do not send what can only bounce, and the document's sender is emailed that the address needs correcting) — the URL is still valid)."
                    }
                  }
                },
                {
                  "$ref": "#/components/schemas/SignerReadback"
                }
              ]
            }
          },
          "field_values_echoed": {
            "$ref": "#/components/schemas/FieldValues"
          },
          "warnings": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/ApiWarning"
                },
                {
                  "$ref": "#/components/schemas/InputWarning"
                }
              ]
            },
            "description": "Unknown top-level keys, plus the validator's `field_values` warnings (an unknown field key — for example a misspelt key — or an unknown table column). Such a value is ignored, never rendered."
          }
        },
        "description": "The direct send (`review` and `validate_only` false), answered with HTTP 200, not 201. Unlike the staged 201 and the dry run, it carries no `template_id`: the template is the one in the request path (and the `signing_request.sent` webhook of this send carries `template_id` and `template_version`)."
      },
      "InstantiateStaged": {
        "type": "object",
        "required": [
          "document_id",
          "metadata",
          "template_id",
          "template_version",
          "status",
          "signing_mode",
          "send_emails",
          "expires_in_days",
          "review_url",
          "review_expires_at",
          "recipients",
          "field_values_echoed",
          "warnings"
        ],
        "properties": {
          "document_id": {
            "type": "string",
            "format": "uuid"
          },
          "metadata": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ApiMetadata"
              },
              {
                "type": "null"
              }
            ],
            "description": "The `metadata` sent on the request, or null."
          },
          "template_id": {
            "type": "string",
            "format": "uuid"
          },
          "template_version": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "const": "staged"
          },
          "signing_mode": {
            "type": "string",
            "enum": [
              "parallel",
              "sequential"
            ]
          },
          "send_emails": {
            "type": "boolean"
          },
          "expires_in_days": {
            "type": "integer"
          },
          "review_url": {
            "type": "string",
            "format": "uri",
            "description": "The review page in the web app: `<app origin>/<locale>/documents/{document_id}/review` (letssign.now today, not the API host). It opens only for a signed-in member of your workspace, so send it to a colleague, never to a signer. There they read the staged document, correct its values (on a PDF template: the Sender / API values, under Edit values) and confirm or discard it. Your integration can confirm or discard with POST /documents/{id}/confirm and /discard instead. The same page is `review.url` on GET /documents/{id} and `review_url` on `template_instance.staged`."
          },
          "review_expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "recipients": {
            "type": "array",
            "description": "No signing URLs: the tokens are inert until a confirm.",
            "items": {
              "allOf": [
                {
                  "type": "object",
                  "required": [
                    "slot",
                    "email",
                    "signing_request_id"
                  ],
                  "properties": {
                    "slot": {
                      "type": "integer"
                    },
                    "email": {
                      "type": "string",
                      "format": "email"
                    },
                    "signing_request_id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                },
                {
                  "$ref": "#/components/schemas/SignerReadback"
                }
              ]
            }
          },
          "field_values_echoed": {
            "$ref": "#/components/schemas/FieldValues"
          },
          "warnings": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "$ref": "#/components/schemas/ApiWarning"
                },
                {
                  "$ref": "#/components/schemas/InputWarning"
                }
              ]
            },
            "description": "Unknown top-level keys and the validator's `field_values` warnings."
          }
        }
      },
      "ConfirmBody": {
        "type": "object",
        "properties": {
          "recipients": {
            "type": "array",
            "maxItems": 20,
            "description": "Per-recipient overrides applied before dispatch. Each entry names the staged signing request it patches.",
            "items": {
              "$ref": "#/components/schemas/ConfirmRecipientPatch"
            }
          },
          "send_emails": {
            "type": "boolean",
            "description": "Overrides the value remembered from the instantiate call. False stops the invitation EMAILS only: the invitation SMS of a recipient with `channel` sms or both still goes out, and so does the SMS code when the signer reaches it. The settled value also governs the later recipients of a sequential document: with false they are not emailed when their turn comes either."
          },
          "expires_in_days": {
            "type": "integer",
            "minimum": 1,
            "maximum": 180,
            "description": "The signer's deadline, re-chosen at confirm so the review wait does not eat the window. Absent, derived from the staged row."
          }
        },
        "description": "Every key is optional; an empty body confirms the instance as staged. Confirm takes NO `field_values`: it sends the values instantiate stored. A `field_values` key is ignored and named in `warnings[]` as `unknown_field` (400 from 2026-12-31). A reviewer corrects values in the app instead, on `review_url` (on a PDF template: its Sender / API values, under Edit values). Confirm then checks the stored values again against the template version the instance was staged from (422 `template_input_invalid`), re-renders a rich-text instance, and copies each Sender / API value onto the signing fields its seal prints, so the signers sign what the reviewer approved."
      },
      "ConfirmRecipientPatch": {
        "type": "object",
        "required": [
          "signing_request_id"
        ],
        "properties": {
          "signing_request_id": {
            "type": "string",
            "format": "uuid"
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          },
          "phone_e164": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\+[1-9]\\d{7,14}$",
            "description": "E.164 (trimmed), e.g. +41791234567; null clears it. The same spelling as Signer and instantiate recipients. Required (stored or supplied) when `channel` includes sms or `require_sms_verification` is true — checked on the merged row (422 `invalid_recipients`)."
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "description": "DEPRECATED alias of `phone_e164`, accepted until 2026-12-31. Each use adds a `deprecated_field` entry to the 200's `warnings[]`. Sending both with different values answers 400 `conflicting_fields`. (Before the 2026-09-24 release this was the only spelling confirm accepted, and `phone_e164` was silently dropped.)",
            "deprecated": true
          },
          "channel": {
            "type": "string",
            "enum": [
              "email",
              "sms",
              "both"
            ]
          },
          "require_sms_verification": {
            "type": "boolean",
            "description": "Second factor by SMS. Available on every tier. Each workspace has a monthly SMS allowance: Free 25, Pro 50, Branded 200, Teams and Enterprise 100 per paid seat, plus any standing raise we set on request (no price per SMS); every code sent counts once, resends included, and the counter resets at 00:00 UTC on the 1st. A create, instantiate or confirm whose SMS signers outnumber the codes left this month is refused with 402 `sms_allowance_exhausted` before anything is created. A request is never downgraded to a signature without SMS. If the allowance runs out after the request was created, the signer cannot get a code until it resets (webhook `signing_request.sms_failed`, `reason: allowance_exhausted`) and cannot sign until then. GET /me reports the allowance as `sms_allowance`."
          },
          "signing_order": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 20
          },
          "sms_gate": {
            "$ref": "#/components/schemas/SmsGate"
          },
          "reference": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "Your own id for this signer; null clears it. See SignerReference."
          },
          "company": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "The company this signer signs for, as on Signer (SignerCompany); null or a value that is blank once trimmed clears the one staged at instantiate. Omit it to keep what was staged."
          },
          "job_title": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120,
            "description": "The signer's function, as on Signer (SignerJobTitle); null or a blank value clears the staged one. Omit it to keep what was staged."
          }
        },
        "additionalProperties": false,
        "description": "Strict: a key other than the ones below answers 400 `unknown_recipient_field` (`meta.recipients` = [{ index, fields }], `meta.accepted`). Only the fields present are written; an untouched recipient keeps what instantiate staged."
      },
      "ConfirmResultEsign": {
        "type": "object",
        "required": [
          "ok",
          "kind",
          "status",
          "already_confirmed",
          "re_rendered",
          "document_id",
          "expires_at",
          "recipients",
          "warnings"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "kind": {
            "type": "string",
            "const": "esign"
          },
          "status": {
            "type": "string",
            "const": "sent"
          },
          "already_confirmed": {
            "type": "boolean",
            "description": "True on a repeat call; `recipients` is then empty."
          },
          "re_rendered": {
            "type": "boolean",
            "description": "True when this confirm rendered the PDF again from the stored values, which the first confirm of a rich-text instance always does. False for a PDF-template instance (its Sender / API values are not rendered into the file: each seal draws them from the signing fields this confirm updated) and on a repeat call."
          },
          "document_id": {
            "type": "string",
            "format": "uuid"
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "recipients": {
            "type": "array",
            "items": {
              "allOf": [
                {
                  "type": "object",
                  "required": [
                    "signing_request_id",
                    "slot",
                    "email",
                    "status",
                    "signing_url",
                    "emailed",
                    "sms_sent"
                  ],
                  "properties": {
                    "signing_request_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "slot": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "email": {
                      "type": "string",
                      "format": "email"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "queued"
                      ],
                      "description": "`queued` = later slot of a sequential document, released but waiting."
                    },
                    "signing_url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "emailed": {
                      "type": "boolean"
                    },
                    "sms_sent": {
                      "type": "boolean",
                      "description": "Whether the invitation SMS (`channel` sms or both) went out — not the verification code, which the signer requests on the sign page."
                    }
                  }
                },
                {
                  "$ref": "#/components/schemas/SignerReadback"
                }
              ]
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiWarning"
            },
            "description": "Unknown top-level keys (`unknown_field`) and one `deprecated_field` per `recipients[i].phone`. Empty when there were none."
          }
        }
      },
      "ConfirmResultFile": {
        "type": "object",
        "required": [
          "ok",
          "kind",
          "status",
          "already_confirmed",
          "re_rendered",
          "document_id",
          "sha256",
          "pages",
          "download_url",
          "warnings"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "kind": {
            "type": "string",
            "const": "file"
          },
          "status": {
            "type": "string",
            "const": "finalized"
          },
          "already_confirmed": {
            "type": "boolean"
          },
          "re_rendered": {
            "type": "boolean"
          },
          "document_id": {
            "type": "string",
            "format": "uuid"
          },
          "sha256": {
            "type": [
              "string",
              "null"
            ]
          },
          "pages": {
            "type": [
              "integer",
              "null"
            ]
          },
          "download_url": {
            "type": "string",
            "format": "uri",
            "description": "`https://api.wesign.now/v1/documents/{id}/pdf` — GET /documents/{id}/pdf on the canonical API host; needs the Bearer key of the owning workspace. Deliberately the current (never signed) render: a file-only instance has no signers, so `/signed` would answer 404 `no_signers`."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiWarning"
            },
            "description": "Unknown top-level keys (`unknown_field`). Empty when there were none."
          }
        }
      },
      "ConfirmError": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "properties": {
              "problems": {
                "type": "array",
                "description": "422 only: `invalid_recipients` → `{ signing_request_id, field, message }[]` with `field` one of signing_request_id, email, phone, channel, require_sms_verification, sms_gate, signing_order; `template_input_invalid` → InputProblem[].",
                "items": {
                  "type": "object",
                  "additionalProperties": true
                }
              },
              "slot": {
                "type": "integer",
                "description": "400 `pinned-slot-mismatch` only."
              }
            }
          }
        ]
      },
      "DiscardResult": {
        "type": "object",
        "required": [
          "ok",
          "status",
          "kind",
          "document_id",
          "already_discarded",
          "withdrawn",
          "deleted"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "status": {
            "type": "string",
            "const": "discarded"
          },
          "kind": {
            "type": "string",
            "enum": [
              "esign",
              "file"
            ]
          },
          "document_id": {
            "type": "string",
            "format": "uuid"
          },
          "already_discarded": {
            "type": "boolean"
          },
          "withdrawn": {
            "type": "integer",
            "description": "Staged signer rows withdrawn (0 for file-only)."
          },
          "deleted": {
            "type": "boolean",
            "description": "True when a file-only instance's document row was removed."
          }
        }
      },
      "EmbeddedSession": {
        "type": "object",
        "required": [
          "id",
          "status",
          "capability",
          "guestId",
          "target",
          "expiresAt",
          "createdAt",
          "consumedAt",
          "revokedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "description": "e.g. pending, consumed, revoked."
          },
          "capability": {
            "type": "string"
          },
          "guestId": {
            "type": "string",
            "format": "uuid"
          },
          "target": {
            "type": "object",
            "required": [
              "kind",
              "id"
            ],
            "properties": {
              "kind": {
                "type": "string"
              },
              "id": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "consumedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "revokedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "EmbedTheme": {
        "type": "object",
        "description": "Host branding for the frame, applied as CSS variables on the embed root. Our own wordmark, navigation and footer are never rendered inside the frame — the chrome around it is yours. Unknown keys are rejected rather than ignored, so a typo is a 400 and not silently-missing branding.",
        "additionalProperties": false,
        "properties": {
          "accent": {
            "type": "string",
            "pattern": "^#[0-9a-fA-F]{6}$",
            "description": "Accent colour as `#rrggbb`. Drives buttons and focus rings.",
            "examples": [
              "#1a73e8"
            ]
          },
          "font": {
            "type": "string",
            "maxLength": 60,
            "description": "A CSS `font-family` list. Sanitised to letters, digits, spaces, commas, quotes and hyphens; quotes must balance. The frame loads no webfont for you — name families the signer's device already has, or ones your own page has nothing to do with loading.",
            "examples": [
              "\"Inter\", system-ui, sans-serif"
            ]
          },
          "logo_url": {
            "type": "string",
            "format": "uri",
            "maxLength": 512,
            "description": "https only, no credentials. Rendered as an `<img>` with `referrerPolicy=\"no-referrer\"`, so we never leak the frame's URL to your CDN."
          },
          "radius": {
            "type": "string",
            "enum": [
              "none",
              "sm",
              "md",
              "lg"
            ],
            "description": "Corner radius scale: 0, 0.375rem, 0.5rem, 0.75rem."
          }
        }
      },
      "EmbedSignSessionCreate": {
        "type": "object",
        "required": [
          "signing_request_id",
          "origin"
        ],
        "properties": {
          "signing_request_id": {
            "type": "string",
            "format": "uuid",
            "description": "An existing signing request in this key's workspace, still `pending` or `viewed` and not past its own `expires_at`. One session = one signer = one document."
          },
          "origin": {
            "type": "string",
            "maxLength": 255,
            "description": "The ancestor origin that will host the frame: scheme, host and optional non-default port, nothing else. Must already be registered on this API key. Accepted in any case and canonicalised (lower-cased, a default `:443` dropped); a path — even a lone `/` — a query, a fragment, credentials, `http:` or a wildcard are refused. The stored value is used verbatim as `frame-ancestors` and as the postMessage `targetOrigin`.",
            "examples": [
              "https://portal.example.com"
            ]
          },
          "ttl_seconds": {
            "type": "integer",
            "minimum": 60,
            "maximum": 900,
            "default": 300,
            "description": "Session lifetime in seconds. Deliberately capped at 15 minutes: mint one when the signer reaches the step, not when the case is created. A value outside the range is a 400, not a clamp."
          },
          "locale": {
            "$ref": "#/components/schemas/Locale"
          },
          "theme": {
            "$ref": "#/components/schemas/EmbedTheme"
          }
        }
      },
      "EmbedSignSessionCreated": {
        "type": "object",
        "required": [
          "id",
          "embed_url",
          "expires_at",
          "signing_request_id"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Session id. Also carried in every postMessage as `session_id`."
          },
          "embed_url": {
            "type": "string",
            "format": "uri",
            "description": "Put this in the iframe's `src` (or load it in the webview). It is on the workspace's own signing host — `https://<slug>.letssign.now` or `https://<slug>.wesign.now` — which is the origin your `frame-src` must name.",
            "examples": [
              "https://acme.letssign.now/de/embed/sign/6b1f0c2e-8f4a-4c1e-9d2b-0a7c5e3f9a41"
            ]
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "After this the frame refuses to render and posts `wesign.expired`. Mint a new session; nothing about the signing request changes."
          },
          "signing_request_id": {
            "type": "string",
            "format": "uuid",
            "description": "Echoed back so the host can key its case state on it — the same id every postMessage carries and the webhook reports."
          }
        }
      },
      "EmbedSignSession": {
        "type": "object",
        "required": [
          "id",
          "status",
          "expires_at",
          "first_seen_at",
          "last_seen_at",
          "signing_request_id"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "expired",
              "revoked"
            ],
            "description": "Lifecycle of the SESSION row. `active` says the permit is still good, not that the document is unsigned."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "first_seen_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the frame was first rendered. `null` means the signer never opened it — useful for telling \"never got there\" apart from \"got there and stopped\"."
          },
          "last_seen_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Most recent render of the frame. `null` until the first."
          },
          "signing_request_id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "WebhookEnvelope": {
        "type": "object",
        "description": "Every delivery body of a hook with `payload: \"full\"` (the default) starts with these four fields; when the event names a `document_id`, `metadata` follows; the event's own fields come at the same level. A hook with `payload: \"minimal\"` receives the Minimal* bodies instead — no `workspace_id`, no `metadata`.",
        "required": [
          "event",
          "event_id",
          "created_at",
          "workspace_id"
        ],
        "properties": {
          "event": {
            "$ref": "#/components/schemas/WebhookEventType"
          },
          "event_id": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]{32}$",
            "description": "Unique per delivery row; the same value rides in X-WeSign-Event-Id."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "workspace_id": {
            "type": "string",
            "format": "uuid"
          },
          "metadata": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ApiMetadata"
              },
              {
                "type": "null"
              }
            ],
            "description": "The `metadata` object given when the document was created through the API (POST /signing-requests, /templates/{id}/instantiate, /generate), echoed on every event that carries a `document_id`; null when the document has none (including every dashboard-created document). Omitted if the lookup failed at emit time, on every delivery to a hook with `payload: \"minimal\"`, and on the test ping (which names no document). Use it to relink a delivery to your own case/record."
          }
        }
      },
      "SigningRequestSentEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "required": [
              "signing_request_id",
              "document_id",
              "signer",
              "role",
              "reference"
            ],
            "properties": {
              "event": {
                "type": "string",
                "const": "signing_request.sent"
              },
              "signing_request_id": {
                "type": "string",
                "format": "uuid"
              },
              "document_id": {
                "type": "string",
                "format": "uuid"
              },
              "signer": {
                "type": "object",
                "required": [
                  "email",
                  "name",
                  "slot"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "role": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "API sends and confirmed reviews: the anchor role, or on a template send the slot's role label. A sequential release: the row's anchor role, else the template slot's role label."
                  },
                  "slot": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "A template send (instantiate, a confirmed review, a dashboard template send): the recipient slot, in either signing mode. POST /signing-requests: the signing order in sequential mode, null in parallel mode. A document sent from the editor in the app (an upload, or a document made from a template): its signing order, or null, as on every later event for that signer."
                  },
                  "reference": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200,
                    "description": "Your `reference` for this signer, or null — on API sends (`source` `v1_api` or `template_instance`) and on every `sequential_release` (null when the document was sent from the app); absent on a dashboard send's first signers. Added in the 2026-09-24 release."
                  },
                  "company": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200,
                    "description": "The company you stated for this signer, or null, on API sends (`source` `v1_api` or `template_instance`) and on every `sequential_release` (null when the document was sent from the app); absent on a dashboard send's first signers."
                  },
                  "job_title": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 120,
                    "description": "The function you stated for this signer, or null, on API sends (`source` `v1_api` or `template_instance`) and on every `sequential_release` (null when the document was sent from the app); absent on a dashboard send's first signers."
                  }
                }
              },
              "locale": {
                "type": "string"
              },
              "source": {
                "type": "string",
                "enum": [
                  "v1_api",
                  "template_instance",
                  "sequential_release"
                ],
                "description": "`v1_api` for API sends, `template_instance` for confirmed reviews, `sequential_release` when a sequential document releases its next signer (added in the 2026-09-25 release); absent for a dashboard send's first signers."
              },
              "template_id": {
                "type": "string",
                "format": "uuid",
                "description": "Template dispatches only."
              },
              "template_version": {
                "type": "integer",
                "description": "Template dispatches only."
              },
              "occurred_at": {
                "type": "string",
                "format": "date-time",
                "description": "`sequential_release` only: when the signer was released. Added in the 2026-09-25 release."
              },
              "emailed": {
                "type": "boolean",
                "description": "`sequential_release` only: whether the invitation email went out. false when the document was created (or its review confirmed) with `send_emails: false` — deliver the signer's link yourself — when the email failed, or when the signer's address is on a domain that accepts no email (a null MX, no such domain, or no mail server — we do not send what can only bounce, and the document's sender is emailed that the address needs correcting); the link works either way. Added in the 2026-09-25 release."
              },
              "test": {
                "type": "boolean",
                "const": true,
                "description": "Not sent since the 2026-10-02 release: the dashboard's Send test now sends a `ping` (PingEvent) to one hook instead. Before, it was set only on the synthetic delivery that button fired.",
                "deprecated": true
              },
              "role": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Top level, for correlation: the signer's role — the `role` sent for this signer on POST /signing-requests, else the template slot's role label (for a template recipient, whose `signer.role` is null); null when there is neither."
              },
              "reference": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 200,
                "description": "Top level, for correlation: your own reference for this signer (`signers[].reference` / `recipients[].reference`), or null — the same value as `signer.reference`."
              }
            }
          }
        ]
      },
      "SigningRequestSignedEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "required": [
              "signing_request_id",
              "document_id",
              "signer",
              "sha256",
              "role",
              "reference"
            ],
            "properties": {
              "event": {
                "type": "string",
                "const": "signing_request.signed"
              },
              "signing_request_id": {
                "type": "string",
                "format": "uuid"
              },
              "document_id": {
                "type": "string",
                "format": "uuid"
              },
              "signer": {
                "$ref": "#/components/schemas/SigningRequestEventSigner"
              },
              "sha256": {
                "type": "string",
                "pattern": "^[0-9a-f]{64}$",
                "description": "SHA-256 of the PDF after this signer's seal."
              },
              "role": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Top level, for correlation: the signer's role — the `role` sent for this signer on POST /signing-requests, else the template slot's role label (for a template recipient, whose `signer.role` is null); null when there is neither."
              },
              "reference": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 200,
                "description": "Top level, for correlation: your own reference for this signer (`signers[].reference` / `recipients[].reference`), or null — the same value as `signer.reference`."
              }
            }
          }
        ]
      },
      "SigningRequestExpiredEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "required": [
              "signing_request_id",
              "document_id",
              "signer",
              "expired_at",
              "role",
              "reference"
            ],
            "properties": {
              "event": {
                "type": "string",
                "const": "signing_request.expired"
              },
              "signing_request_id": {
                "type": "string",
                "format": "uuid"
              },
              "document_id": {
                "type": "string",
                "format": "uuid"
              },
              "signer": {
                "$ref": "#/components/schemas/SigningRequestEventSigner"
              },
              "expired_at": {
                "type": "string",
                "format": "date-time",
                "description": "The request's `expires_at` — the moment the link stopped working, not the cron tick that recorded it."
              },
              "role": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Top level, for correlation: the signer's role — the `role` sent for this signer on POST /signing-requests, else the template slot's role label (for a template recipient, whose `signer.role` is null); null when there is neither."
              },
              "reference": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 200,
                "description": "Top level, for correlation: your own reference for this signer (`signers[].reference` / `recipients[].reference`), or null — the same value as `signer.reference`."
              }
            }
          }
        ]
      },
      "DocumentCompletedEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "required": [
              "signing_request_id",
              "document_id",
              "signer",
              "signed_pdf_url",
              "audit_trail_url",
              "sha256",
              "cert_serial",
              "tsa_provider",
              "tsa_signed_at"
            ],
            "properties": {
              "event": {
                "type": "string",
                "const": "document.completed"
              },
              "signing_request_id": {
                "type": "string",
                "format": "uuid",
                "description": "The final signer's request."
              },
              "document_id": {
                "type": "string",
                "format": "uuid"
              },
              "signer": {
                "$ref": "#/components/schemas/SigningRequestEventSigner"
              },
              "signed_pdf_url": {
                "type": "string",
                "format": "uri",
                "description": "`https://api.wesign.now/v1/documents/{document_id}/signed` — key-authenticated (the owning workspace's Bearer key). 409 `not_complete` until every signer signed; 404 across workspaces; 410 `deleted_by_retention` once the workspace's retention period has passed, so fetch it on this event and keep your own copy."
              },
              "audit_trail_url": {
                "type": "string",
                "format": "uri",
                "description": "`https://api.wesign.now/v1/documents/{document_id}/audit-trail` — the audit-trail PDF; same host, same Bearer key and same 409/404 rules as `signed_pdf_url`, but never 410: it stays available after retention."
              },
              "sha256": {
                "type": "string",
                "pattern": "^[0-9a-f]{64}$",
                "description": "Lowercase hex SHA-256 of exactly the bytes served at `signed_pdf_url` (the same value as the `sha256` of the last signer's full-body `signing_request.signed`). Not the `sha256` of GET /documents/{id}, which hashes the PDF as it was sent for signature."
              },
              "cert_serial": {
                "type": "string"
              },
              "tsa_provider": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Name of the RFC 3161 timestamp authority that stamped the sealed PDF, e.g. \"DigiCert\" (a fallback authority such as \"freetsa.org\" when the primary is unavailable). Informational: do not branch on it. Null when no timestamp could be obtained."
              },
              "tsa_signed_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "The time inside the timestamp token (TSTInfo genTime), in whole seconds UTC. Every token is verified before it is kept: it must cover the sealed PDF's SHA-256 and our request nonce, and be signed by the authority's timestamping certificate under a pinned root."
              }
            }
          }
        ]
      },
      "TemplateInstanceStagedEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "required": [
              "document_id",
              "metadata",
              "template_id",
              "template_version",
              "kind",
              "review_url",
              "review_expires_at",
              "source",
              "api_key_id"
            ],
            "properties": {
              "event": {
                "type": "string",
                "const": "template_instance.staged"
              },
              "document_id": {
                "type": "string",
                "format": "uuid"
              },
              "metadata": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/ApiMetadata"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "The `metadata` sent on the instantiate/generate call, or null. Both kinds (esign from /instantiate, file from /generate) carry it."
              },
              "template_id": {
                "type": "string",
                "format": "uuid"
              },
              "template_version": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "kind": {
                "type": "string",
                "enum": [
                  "esign",
                  "file"
                ]
              },
              "review_url": {
                "type": "string",
                "format": "uri",
                "description": "The review page in the web app for a signed-in workspace member; see InstantiateStaged.review_url."
              },
              "review_expires_at": {
                "type": "string",
                "format": "date-time"
              },
              "filename": {
                "type": "string",
                "description": "kind `file` only."
              },
              "signing_mode": {
                "type": "string",
                "enum": [
                  "parallel",
                  "sequential"
                ],
                "description": "kind `esign` only."
              },
              "recipients": {
                "type": "array",
                "description": "kind `esign` only.",
                "items": {
                  "type": "object",
                  "required": [
                    "signing_request_id",
                    "slot",
                    "email"
                  ],
                  "properties": {
                    "signing_request_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "slot": {
                      "type": "integer"
                    },
                    "email": {
                      "type": "string",
                      "format": "email"
                    }
                  }
                }
              },
              "source": {
                "type": "string",
                "const": "v1_api"
              },
              "api_key_id": {
                "type": "string",
                "format": "uuid"
              }
            }
          }
        ]
      },
      "TemplateInstanceConfirmedEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "required": [
              "document_id",
              "template_id",
              "template_version",
              "kind",
              "actor"
            ],
            "properties": {
              "event": {
                "type": "string",
                "const": "template_instance.confirmed"
              },
              "document_id": {
                "type": "string",
                "format": "uuid"
              },
              "template_id": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uuid"
              },
              "template_version": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "kind": {
                "type": "string",
                "enum": [
                  "esign",
                  "file"
                ]
              },
              "actor": {
                "type": "string",
                "enum": [
                  "user",
                  "api_key",
                  "system"
                ]
              },
              "expires_at": {
                "type": "string",
                "format": "date-time",
                "description": "kind `esign` only."
              },
              "recipients": {
                "type": "array",
                "description": "kind `esign` only.",
                "items": {
                  "type": "object",
                  "required": [
                    "signing_request_id",
                    "slot",
                    "email",
                    "status"
                  ],
                  "properties": {
                    "signing_request_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "slot": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "email": {
                      "type": "string",
                      "format": "email"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "queued"
                      ]
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "TemplateInstanceDiscardedEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "required": [
              "document_id",
              "template_id",
              "kind",
              "reason",
              "actor"
            ],
            "properties": {
              "event": {
                "type": "string",
                "const": "template_instance.discarded"
              },
              "document_id": {
                "type": "string",
                "format": "uuid"
              },
              "template_id": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uuid"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "esign",
                  "file"
                ]
              },
              "reason": {
                "type": "string",
                "enum": [
                  "discarded",
                  "expired"
                ],
                "description": "`expired` when the 14-day review window lapsed (cron)."
              },
              "actor": {
                "type": "string",
                "enum": [
                  "user",
                  "api_key",
                  "system"
                ]
              }
            }
          }
        ]
      },
      "SmsGate": {
        "type": "string",
        "enum": [
          "before_sign",
          "before_view"
        ],
        "default": "before_sign",
        "description": "When the signer must enter the SMS code. `before_sign` (default): the signer opens and reads the document and enters the code at the Sign press. `before_view`: the signer sees nothing of the document — no title, file name, pages or prefilled values, and not their own name or email — until the code is verified; the code is texted when the signer taps \"Text me a code\" on that page, never on page load (so a link scanner sends none); after a reload within 10 minutes the page shows the code field again instead of sending another. Every invitation email for the request (reminders included) leaves the document unnamed (subject \"Document to sign from {sender}\"), and so does the invitation SMS of confirm `channel` sms/both. The signing page's own document endpoints answer 403 `sms_verification_required` until the code is verified (they are not part of this API; you only meet the code in a browser). `before_view` requires `require_sms_verification: true` (400 on create and instantiate; 422 `invalid_recipients` at field `sms_gate` on confirm, where the edited row is checked). Embedded signing does not support `before_view` yet: the frame shows its unavailable card and posts `wesign.error` with detail `sms_required`."
      },
      "SignerReference": {
        "type": "string",
        "maxLength": 200,
        "description": "Your own id for this signer (≤ 200 characters). Stored on the signing request and returned as `reference` by create, instantiate, confirm and GET /signing-requests/{id}, and as `signer.reference` on `signing_request.sent` (API sends), `signing_request.signed`, `document.completed` and the six signer events (viewed, declined, withdrawn, sms_sent, sms_failed, sms_verified). Not carried by `signing_request.expired` — correlate that one by `signing_request_id`."
      },
      "SignerCompany": {
        "type": "string",
        "maxLength": 200,
        "description": "The company this signer signs for, as you state it (e.g. \"Muster AG\"). We never verify it. Runs of whitespace collapse to one space and the ends are trimmed; a value that is blank after that means none. At most 200 characters; a longer value, or one with a line break, tab or other control character, answers 400 `invalid_request`. Printed on line 1 of the caption under each of the signer's signature and initials boxes (`{name} · {job_title} · {company} · {email}`, shrunk to fit the box down to 6 pt, else with function and company on a line of their own under the name; never cut, never off the page) and on the audit certificate, the one attached to the completion emails and the one from GET /documents/{id}/audit-trail alike, as \"Company (stated by the sender)\". Read back as `company`. It does not prefill a text box keyed `company.legal_name`; that comes from the workspace address book."
      },
      "SignerJobTitle": {
        "type": "string",
        "maxLength": 120,
        "description": "The signer's function as it should print (e.g. \"Geschäftsführerin\", \"Mitglied des Verwaltungsrates\"), as you state it. We never verify it. The same rules as `company`, at most 120 characters. Printed between the name and the company on line 1 of the caption and on the audit certificate (emailed and from GET /documents/{id}/audit-trail) as \"Function (stated by the sender)\". Read back as `job_title`."
      },
      "SignerReadback": {
        "type": "object",
        "description": "What a signing request was created with, read back per signer: the SMS factor, when the code is asked for, the resulting signature level, your reference, and the company and function you stated. Identical on the create response, instantiate (direct and staged), confirm and GET /signing-requests/{id}, so a factor that did not arrive is visible at once. GET /documents/{id} `signers[]` carries `company` and `job_title` but not the rest.",
        "required": [
          "require_sms_verification",
          "phone_masked",
          "sms_verified_at",
          "sms_gate",
          "signature_level",
          "reference",
          "company",
          "job_title"
        ],
        "properties": {
          "require_sms_verification": {
            "type": "boolean"
          },
          "phone_masked": {
            "type": [
              "string",
              "null"
            ],
            "description": "The stored phone masked the way the signer sees it — the last four digits only. Null when no phone is stored. The full number is never returned.",
            "examples": [
              "•••• •••• 7037"
            ]
          },
          "sms_verified_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the signer entered the correct code; null until then (always null on a create response)."
          },
          "sms_gate": {
            "$ref": "#/components/schemas/SmsGate"
          },
          "signature_level": {
            "type": "string",
            "enum": [
              "AES",
              "SES"
            ],
            "description": "`AES` when `require_sms_verification` is true (the SMS code binds the signature to a phone), otherwise `SES` (the email link alone). It states the level the request was created for; whether the code has been entered is `sms_verified_at`."
          },
          "reference": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "Your `reference` for this signer, or null."
          },
          "company": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "The `company` you stated for this signer, as stored (whitespace collapsed, trimmed); null when none was given. Never verified."
          },
          "job_title": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120,
            "description": "The `job_title` you stated for this signer, as stored; null when none was given. Never verified."
          }
        }
      },
      "ApiWarning": {
        "type": "object",
        "description": "A non-blocking notice about the request itself. `unknown_field`: a top-level key this endpoint does not know was ignored — from 2026-12-31 it is refused with 400 `unknown_field`. `deprecated_field`: a deprecated spelling was accepted (confirm `recipients[i].phone`, accepted until 2026-12-31). The template validator's InputWarning shares the array and the shape, and also uses the code `unknown_field` — for a `field_values` key; `field` tells them apart (a top-level body key vs. a template field key).",
        "required": [
          "field",
          "code",
          "message"
        ],
        "properties": {
          "field": {
            "type": "string",
            "description": "The top-level key (`send_email`) or the path of the deprecated field (`recipients[0].phone`)."
          },
          "code": {
            "type": "string",
            "enum": [
              "unknown_field",
              "deprecated_field"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "SmsAllowance": {
        "type": "object",
        "description": "The workspace's monthly SMS allowance. One code sent = one unit, resends included.",
        "required": [
          "monthly",
          "used",
          "remaining",
          "resets_at"
        ],
        "properties": {
          "monthly": {
            "type": "integer",
            "minimum": 0,
            "description": "The codes this workspace may send per month: its plan's (Free 25, Pro 50, Branded 200, Teams/Enterprise 100 × paid seats) plus any standing raise we set on request. Every 402 `sms_allowance_exhausted` check uses this number."
          },
          "used": {
            "type": "integer",
            "minimum": 0,
            "description": "Codes sent since the last reset."
          },
          "remaining": {
            "type": "integer",
            "minimum": 0
          },
          "resets_at": {
            "type": "string",
            "format": "date-time",
            "description": "00:00 UTC on the 1st of next month."
          }
        }
      },
      "SmsAllowanceExhausted": {
        "type": "object",
        "description": "402 body: the signers of this call who must verify by SMS outnumber the codes the workspace has left this month. Nothing was created, stored or sent. The extra fields sit at the top level, not under `meta`. The check guarantees each SMS signer one code; a resend still costs one more.",
        "required": [
          "error",
          "code",
          "sms_allowance",
          "required"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "const": "sms_allowance_exhausted"
          },
          "sms_allowance": {
            "$ref": "#/components/schemas/SmsAllowance"
          },
          "required": {
            "type": "integer",
            "minimum": 1,
            "description": "SMS signers in this call (queued sequential followers and, on confirm, recipients released for later included)."
          }
        },
        "examples": [
          {
            "error": "This request has 2 signers who must verify by SMS, but the workspace has 0 of 300 SMS codes left this month (resets 2026-10-01T00:00:00.000Z). Nothing was created or sent.",
            "code": "sms_allowance_exhausted",
            "sms_allowance": {
              "monthly": 300,
              "used": 300,
              "remaining": 0,
              "resets_at": "2026-10-01T00:00:00.000Z"
            },
            "required": 2
          }
        ]
      },
      "EnterpriseRequiredBody": {
        "type": "object",
        "required": [
          "error",
          "code"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "const": "enterprise-required"
          }
        }
      },
      "SigningRequestEventSigner": {
        "type": "object",
        "required": [
          "email",
          "name",
          "role",
          "slot",
          "reference",
          "company",
          "job_title"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "role": {
            "type": [
              "string",
              "null"
            ],
            "description": "The signer's `role` on POST /signing-requests; null for template recipients (use `slot`)."
          },
          "slot": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Template slot, else signing order; null in a parallel POST /signing-requests send. The same value `signing_request.sent` carried for this signer."
          },
          "reference": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "Your own reference for this signer (`signers[].reference` / `recipients[].reference`), or null."
          },
          "company": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "The company you stated for this signer (`signers[].company` / `recipients[].company`), or null. Never verified."
          },
          "job_title": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120,
            "description": "The function you stated for this signer (`job_title`), or null. Never verified."
          }
        },
        "description": "The `signer` block of every signing_request.* event except a dashboard send's `signing_request.sent`, and of `document.completed`: `signing_request.signed`, `signing_request.expired` (until the 2026-10-02 release it carried only { email, name }), `document.completed` and the six signer events (viewed, declined, withdrawn, sms_sent, sms_failed, sms_verified). Every key is always present. `name` is the stored display name. If the signer row cannot be read when `signing_request.signed` or `document.completed` is built, their block carries `email` and `name` and null for the rest rather than holding the event back. Hooks with `payload: \"minimal\"` never receive it."
      },
      "SigningRequestEventBase": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "required": [
              "signing_request_id",
              "document_id",
              "signer",
              "occurred_at",
              "role",
              "reference"
            ],
            "properties": {
              "signing_request_id": {
                "type": "string",
                "format": "uuid"
              },
              "document_id": {
                "type": "string",
                "format": "uuid"
              },
              "signer": {
                "$ref": "#/components/schemas/SigningRequestEventSigner"
              },
              "occurred_at": {
                "type": "string",
                "format": "date-time",
                "description": "When the transition happened. `created_at` is when this delivery was queued."
              },
              "role": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Top level, for correlation: the signer's role — the `role` sent for this signer on POST /signing-requests, else the template slot's role label (for a template recipient, whose `signer.role` is null); null when there is neither."
              },
              "reference": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 200,
                "description": "Top level, for correlation: your own reference for this signer (`signers[].reference` / `recipients[].reference`), or null — the same value as `signer.reference`."
              }
            }
          }
        ],
        "description": "The shared base of the six signer events added in the 2026-09-24 release (viewed, declined, withdrawn, sms_sent, sms_failed, sms_verified). Every signing_request.* event also carries `role` and `reference` at the top level (2026-10-02 release)."
      },
      "SigningRequestViewedEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SigningRequestEventBase"
          },
          {
            "type": "object",
            "properties": {
              "event": {
                "type": "string",
                "const": "signing_request.viewed"
              }
            }
          }
        ]
      },
      "SigningRequestDeclinedEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SigningRequestEventBase"
          },
          {
            "type": "object",
            "required": [
              "reason"
            ],
            "properties": {
              "event": {
                "type": "string",
                "const": "signing_request.declined"
              },
              "reason": {
                "type": [
                  "string",
                  "null"
                ],
                "maxLength": 1000,
                "description": "What the signer typed, trimmed; null if they gave none. Cut at 1000 characters (the signing page accepts 2000); the full text is in GET /signing-requests/{id} `auditEvents[type=declined].meta.reason`."
              }
            }
          }
        ]
      },
      "SigningRequestWithdrawnEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SigningRequestEventBase"
          },
          {
            "type": "object",
            "required": [
              "withdrawn_by"
            ],
            "properties": {
              "event": {
                "type": "string",
                "const": "signing_request.withdrawn"
              },
              "withdrawn_by": {
                "type": "string",
                "enum": [
                  "sender",
                  "api"
                ],
                "description": "`api` = your key via POST /signing-requests/{id}/withdraw; `sender` = a user in the dashboard (one request, or a whole envelope)."
              }
            }
          }
        ]
      },
      "SigningRequestSmsSentEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SigningRequestEventBase"
          },
          {
            "type": "object",
            "required": [
              "phone_masked",
              "attempt",
              "sms_allowance"
            ],
            "properties": {
              "event": {
                "type": "string",
                "const": "signing_request.sms_sent"
              },
              "phone_masked": {
                "type": "string",
                "examples": [
                  "•••• •••• 7037"
                ]
              },
              "attempt": {
                "type": [
                  "integer",
                  "null"
                ],
                "minimum": 1,
                "description": "Codes sent for this signing request so far, this one included; null if the count could not be read."
              },
              "sms_allowance": {
                "$ref": "#/components/schemas/SmsAllowance",
                "description": "The workspace's allowance after this code."
              }
            }
          }
        ]
      },
      "SigningRequestSmsFailedEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SigningRequestEventBase"
          },
          {
            "type": "object",
            "required": [
              "reason",
              "phone_masked",
              "sms_allowance"
            ],
            "properties": {
              "event": {
                "type": "string",
                "const": "signing_request.sms_failed"
              },
              "reason": {
                "type": "string",
                "enum": [
                  "invalid_number",
                  "landline",
                  "blocked",
                  "rate_limited",
                  "allowance_exhausted",
                  "not_configured",
                  "provider_error"
                ],
                "description": "`invalid_number` — no usable E.164 number stored, or the SMS provider rejected the number as invalid. `landline` — the provider identified a landline. `blocked` — the destination is not enabled for our SMS service, is embargoed, or the provider's fraud protection blocked the number's prefix (temporarily, 12 h). `rate_limited` — too many codes for this number without a completed check (clears when the 10-minute code expires) or provider throttling. `allowance_exhausted` — the workspace's monthly SMS allowance is used up; the signer cannot sign until it resets. `not_configured` — no SMS provider on this deployment (never in production). `provider_error` — anything else. Clicks our own per-IP throttle refuses (4 code requests per minute) do not emit."
              },
              "phone_masked": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "null when no usable E.164 number is stored."
              },
              "sms_allowance": {
                "$ref": "#/components/schemas/SmsAllowance"
              }
            }
          }
        ]
      },
      "SigningRequestSmsVerifiedEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SigningRequestEventBase"
          },
          {
            "type": "object",
            "required": [
              "phone_masked",
              "sms_verified_at"
            ],
            "properties": {
              "event": {
                "type": "string",
                "const": "signing_request.sms_verified"
              },
              "phone_masked": {
                "type": "string"
              },
              "sms_verified_at": {
                "type": "string",
                "format": "date-time",
                "description": "The stamp the AES signature relies on — the same value as `sms_verified_at` on GET /signing-requests/{id}."
              }
            }
          }
        ]
      },
      "HookDelivery": {
        "type": "object",
        "description": "One delivery of a hook, as GET /hooks/{id}/deliveries lists it. The body we sent is not included. Property order as listed.",
        "required": [
          "id",
          "event",
          "event_id",
          "document_id",
          "attempts",
          "response_status",
          "response_body",
          "latency_ms",
          "status",
          "created_at",
          "next_retry_at",
          "completed_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The delivery id — what POST /hooks/{id}/deliveries/{delivery_id}/redeliver takes."
          },
          "event": {
            "type": "string",
            "description": "A WebhookEventType, or `ping` for a test delivery."
          },
          "event_id": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]{32}$",
            "description": "The delivery's `event_id` — the same value as the body field and X-WeSign-Event-Id, identical on every attempt and on a redelivery."
          },
          "document_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The `document_id` of the delivered event; null for a ping."
          },
          "attempts": {
            "type": "integer",
            "minimum": 0,
            "description": "Attempts made so far (0 = queued, not yet attempted). The ladder is 8 attempts."
          },
          "response_status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "HTTP status your endpoint answered on the last attempt; null when no response came (timeout, connection error, address refused) or before the first attempt. A 3xx is a failure: redirects are not followed."
          },
          "response_body": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 500,
            "description": "The first 500 characters of your endpoint's last response body — or our note when there was none: `network: …` for a timeout or connection error, `redirect not followed: HTTP <status>, Location: <url>` before the body of a 3xx."
          },
          "latency_ms": {
            "type": [
              "integer",
              "null"
            ],
            "description": "How long the last attempt took, in milliseconds; null before the first attempt (or for deliveries made before the 2026-10-02 release)."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "success",
              "failed",
              "giving_up"
            ],
            "description": "`success`: your endpoint answered 2xx. `pending`: queued or waiting for its next retry (`next_retry_at`). `giving_up`: every attempt failed — or the hook was disabled before it could be sent, or a test ping failed (never retried); POST …/redeliver sends it again. `failed` is reserved and not currently written."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the event was queued for this hook."
          },
          "next_retry_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the next attempt is due; null once delivered or given up."
          },
          "completed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it was delivered or given up; null while pending."
          }
        }
      },
      "HookRedelivered": {
        "type": "object",
        "required": [
          "delivery_id",
          "event_id",
          "status",
          "response_status",
          "latency_ms"
        ],
        "properties": {
          "delivery_id": {
            "type": "string",
            "format": "uuid"
          },
          "event_id": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]{32}$",
            "description": "The delivery's `event_id` — the same value as the body field and X-WeSign-Event-Id, identical on every attempt and on a redelivery."
          },
          "status": {
            "type": "string",
            "enum": [
              "success",
              "pending",
              "giving_up"
            ],
            "description": "`success`: your endpoint answered 2xx. `pending`: it did not; the delivery carries on along the retry schedule from its attempt count. `giving_up`: it did not, and the delivery had no retries left (redeliver again when your endpoint is fixed)."
          },
          "response_status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "HTTP status your endpoint answered; null when no response came (timeout, connection error, address refused)."
          },
          "latency_ms": {
            "type": [
              "integer",
              "null"
            ],
            "description": "How long the attempt took, in milliseconds; null when it could not be made."
          }
        },
        "description": "The POST /hooks/{id}/deliveries/{delivery_id}/redeliver response: the outcome of the one attempt made at once. The delivery log shows the same row."
      },
      "HookTestResult": {
        "type": "object",
        "required": [
          "delivery_id",
          "event_id",
          "status",
          "response_status",
          "latency_ms"
        ],
        "properties": {
          "delivery_id": {
            "type": "string",
            "format": "uuid"
          },
          "event_id": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]{32}$",
            "description": "The delivery's `event_id` — the same value as the body field and X-WeSign-Event-Id, identical on every attempt and on a redelivery."
          },
          "status": {
            "type": "string",
            "enum": [
              "success",
              "giving_up"
            ],
            "description": "`success`: your endpoint answered 2xx. `giving_up`: it did not — a test is never retried."
          },
          "response_status": {
            "type": [
              "integer",
              "null"
            ],
            "description": "HTTP status your endpoint answered; null when no response came (timeout, connection error, address refused)."
          },
          "latency_ms": {
            "type": [
              "integer",
              "null"
            ],
            "description": "How long the attempt took, in milliseconds; null when it could not be made."
          }
        },
        "description": "The POST /hooks/{id}/test response: the outcome of the one ping attempt. The delivery log shows it as event `ping`."
      },
      "MinimalSigningRequestEvent": {
        "type": "object",
        "additionalProperties": false,
        "description": "The body of every signing_request.* event for a hook with `payload: \"minimal\"`: exactly these keys — no names, emails, phone numbers, decline reason, metadata or workspace_id. Same shape for signed, declined, expired and the others.",
        "required": [
          "event",
          "event_id",
          "created_at",
          "document_id",
          "signing_request_id",
          "role",
          "reference"
        ],
        "properties": {
          "event": {
            "type": "string",
            "pattern": "^signing_request\\."
          },
          "event_id": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]{32}$",
            "description": "Unique per event and hook, identical on every retry; the same value rides in X-WeSign-Event-Id."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the event was queued."
          },
          "document_id": {
            "type": "string",
            "format": "uuid",
            "description": "The document — `documentId` of the POST /signing-requests response, `document_id` of instantiate."
          },
          "signing_request_id": {
            "type": "string",
            "format": "uuid",
            "description": "The signer's request — `signers[].signingRequestId` of the create response."
          },
          "role": {
            "type": [
              "string",
              "null"
            ],
            "description": "Top level, for correlation: the signer's role — the `role` sent for this signer on POST /signing-requests, else the template slot's role label (for a template recipient, whose `signer.role` is null); null when there is neither."
          },
          "reference": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "description": "Top level, for correlation: your own reference for this signer (`signers[].reference` / `recipients[].reference`), or null — the same value as `signer.reference`."
          }
        }
      },
      "MinimalDocumentCompletedEvent": {
        "type": "object",
        "additionalProperties": false,
        "description": "The body of `document.completed` for a hook with `payload: \"minimal\"`: exactly these keys.",
        "required": [
          "event",
          "event_id",
          "created_at",
          "document_id",
          "signed_pdf_url",
          "sha256",
          "audit_trail_url"
        ],
        "properties": {
          "event": {
            "type": "string",
            "const": "document.completed"
          },
          "event_id": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]{32}$",
            "description": "Unique per event and hook, identical on every retry; the same value rides in X-WeSign-Event-Id."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the event was queued."
          },
          "document_id": {
            "type": "string",
            "format": "uuid",
            "description": "The document — `documentId` of the POST /signing-requests response, `document_id` of instantiate."
          },
          "signed_pdf_url": {
            "type": "string",
            "format": "uri",
            "description": "`https://api.wesign.now/v1/documents/{document_id}/signed` — GET with the workspace's Bearer key; the same bytes on every fetch until the workspace's retention period ends (then 410 `deleted_by_retention`), so fetch it on this event and keep your own copy."
          },
          "sha256": {
            "type": "string",
            "pattern": "^[0-9a-f]{64}$",
            "description": "Lowercase hex SHA-256 of exactly the bytes served at `signed_pdf_url`. Not the `sha256` of GET /documents/{id}, which hashes the PDF as it was sent for signature."
          },
          "audit_trail_url": {
            "type": "string",
            "format": "uri",
            "description": "`https://api.wesign.now/v1/documents/{document_id}/audit-trail` — the audit trail, a SEPARATE PDF (not inside the signed PDF), rendered on each request (its bytes are not covered by `sha256`); never 410, it stays available after retention."
          }
        }
      },
      "MinimalDocumentEvent": {
        "type": "object",
        "additionalProperties": false,
        "description": "The body of a template_instance.* event for a hook with `payload: \"minimal\"`: exactly these keys.",
        "required": [
          "event",
          "event_id",
          "created_at",
          "document_id"
        ],
        "properties": {
          "event": {
            "type": "string",
            "pattern": "^template_instance\\."
          },
          "event_id": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]{32}$",
            "description": "Unique per event and hook, identical on every retry; the same value rides in X-WeSign-Event-Id."
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the event was queued."
          },
          "document_id": {
            "type": "string",
            "format": "uuid",
            "description": "The document — `documentId` of the POST /signing-requests response, `document_id` of instantiate."
          }
        }
      },
      "PingEvent": {
        "type": "object",
        "additionalProperties": false,
        "description": "The body POST /hooks/{id}/test (and the dashboard's Send test) delivers to one hook, signed like every delivery. `ping` is not an event type a hook subscribes to; answer 2xx to event names you do not handle.",
        "required": [
          "event",
          "event_id",
          "created_at"
        ],
        "properties": {
          "event": {
            "type": "string",
            "const": "ping"
          },
          "event_id": {
            "type": "string",
            "pattern": "^evt_[0-9a-f]{32}$",
            "description": "Unique per event and hook, identical on every retry; the same value rides in X-WeSign-Event-Id."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "Documentation: guides, reference, changelog and versioning policy (machine-readable changelog: https://www.wesign.now/api-changelog.json)",
    "url": "https://www.wesign.now/docs"
  }
}
