{
  "openapi": "3.1.0",
  "info": {
    "title": "wesign.now API",
    "version": "2026-10-22",
    "summary": "Send PDFs and locked templates for e-signature, poll status, fetch sealed PDFs and audit trails, subscribe to webhooks.",
    "description": "wesign.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 its `audit_trail_sha256` with the audit trail's, 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: generate a client from this document. AI agents can also use the MCP server at https://www.wesign.now/api/mcp (Streamable HTTP, the same `Authorization: Bearer <api key>`; https://www.wesign.now/docs/mcp): its tools send and track on this API's caps, rate limit and error codes, and never sign.\n\nHOSTS, KEYS AND LIMITS. The host is https://api.wesign.now/v1. Every request carries `Authorization: Bearer <api key>` — every plan can create keys, in the workspace's developer settings; they look like `wsk_live_<32 hex>` (keys minted before September 2026 look like `lsk_live_<32 hex>` and are accepted forever). The documents a workspace creates through the API count toward a monthly cap per plan: past it, POST /signing-requests answers 402 `tier_required` (the plan's overall monthly document cap) or `api_monthly_limit` (the plan's monthly cap on documents created through the API), both with `meta: { tier, cap, used }`. Errors are JSON `{ error, code, meta? }` with a stable `code`; `error` says what was wrong and what to do, and a 400 from a schema lists every problem with its field in `meta.problems: [{ path, message }]`. Every answer under /v1 carries an `X-Request-Id` response header — the id of the call in the API log (Settings → Developers → API log, 30 days, no request or response bodies stored); quote it to support with the time of the call. Every answer under /v1 is JSON, a typo included: a path no endpoint has answers 404 `unknown_endpoint` (`meta.did_you_mean` names the closest endpoints with their method), a method an endpoint does not have answers 405 `method_not_allowed` (`meta.allowed` and an `Allow` header) — both without a key and without touching the rate limit (components.responses UnknownEndpoint and MethodNotAllowed). A key may additionally be restricted to a list of caller IP addresses/CIDR blocks (Settings → Developers); 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 (00:00 UTC) it is refused with 400 `unknown_field`. On POST /signing-requests an unknown key inside an explicit field or a signer's `role_label` object is named by its path (`fields[0].type`) and refused the same way from 2027-01-31 (00:00 UTC).\n\nLANGUAGES. Give `locale` per signer (`signers[].locale` on POST /signing-requests, `recipients[].locale` on instantiate) to choose the language of that signer's email, SMS code and signing page. It is optional, on the request and on every signer, and a call without any language is never refused. Without one, we use the request's `locale`, then the language remembered for this person in your workspace, then your workspace language, then English; the signing page then follows the signer's browser language. A signer who has their own wesign.now account with a language set always gets that language; every response and webhook shows the language you asked for. The full rule is on the Locale schema.\n\nSEAL CERTIFICATE. From 2026-10-22, 00:00 UTC, new seals are made with a new certificate, `CN=wesign.now Signing Authority, O=wesign.now, OU=E-Signature Service, C=CH` (RSA-3072, SHA-256 fingerprint `8a3d0158b768dd0d4244b4e20c6d1cbd99b1de4e651842a170ed42195bcb5d6d`), in PAdES baseline B-T (`ETSI.CAdES.detached`, the RFC 3161 time stamp embedded in the signature); seals made before keep the previous seal certificate (SHA-256 fingerprint `0568a78abeb5b81e28c2ee4184a0d33b7b93f74647967c8e5b28391de626bcdc`) and stay valid. Nothing changes shape: `cert_serial` names whichever certificate sealed. Validate the signature and compare its certificate with https://www.wesign.now/seal-certificates.json rather than pinning one certificate.\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"
    }
  ],
  "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"
    ],
    "note": "Sent on every delivery. Verify X-WeSign-Signature before trusting the body.",
    "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."
    },
    "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 → Developers. 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.",
    "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_note": "The User-Agent names our webhook sender and is not part of the contract: never filter deliveries on it, verify X-WeSign-Signature.",
    "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 → Developers → Fields & API objects.",
        "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 X-WeSign-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 — an A4 portrait page, unrotated and uncropped, whatever the upload's own pages set; `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. Anchored fields get the caption check of ExplicitField as warnings only, never refused (`field` = the marker as the PDF spells it, e.g. `[[ls:signature:tenant]]`); the `auto_append` page is not checked.\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. A signer may carry its own `locale`: one document can go to one signer in English and another in German (Locale). An unknown top-level key is named in `warnings[]` until 2026-12-31, an unknown key inside a field or `role_label` until 2027-01-31; from those days (00:00 UTC) the call is 400 `unknown_field` (`meta.fields`, `meta.accepted`), after an Idempotency-Key replay and before anything is stored.\n\nYour file: the PDF is decrypted when it is only restricted against printing or copying (one that needs its open password is 422 `pdf_user_password_required`), its fillable form fields are baked into the page and it is normally re-written by our PDF engine before fields are placed; GET /documents/{id} lists what was done in `ingestNotes`, and its `sha256` is that of the prepared copy, never of your upload. A PDF that already carries a digital signature cannot keep it — the copy presented for signing breaks it and the seal removes it, only its visible appearance stays: it is named in `warnings[]` (`existing_signature_removed`) and refused with 422 `pdf_already_signed` from 2027-01-31 (00:00 UTC).\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` (the body is not valid JSON, or failed the schema — every problem with its field in `error` and in `meta.problems: [{ path, message }]` — 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), `unknown_field` (from 2026-12-31 an unknown top-level key; from 2027-01-31 also a key inside a field or `role_label` — `meta.fields` every refused key or path, `meta.accepted` what is accepted there; `warnings[]` carries notices not refused yet; an `Idempotency-Key` replay of a call created before that day still answers its 201), `placement_retired` (placement=\"manual\"), `duplicate_role` (two or more signers share a `role` — fields are assigned by role, so each signer needs its own; `meta.role` the shared role, `meta.emails` every signer that carries it; refused in every placement mode since revision 2026-10-17), `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 }`); `api_monthly_limit` — the plan's monthly cap on documents created through the API is reached (`meta: { tier, cap, used }`; wait for the next month or upgrade; Enterprise has no API cap); 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`), `pdf_user_password_required` (the PDF needs its open password — send a copy without it), `pdf_decrypt_failed` (its protection could not be removed — send an unprotected copy), `pdf_already_signed` (from 2027-01-31: the PDF already carries a digital signature, which preparing it would remove; `meta.signatures` = `[ExistingSignature]`; until then a warning), `placement_failed` (PDF mutation failed), `page_geometry_unsupported` (explicit placement: a page carrying a field has no visible frame wesign can place on exactly — `meta.reason` is `crop_box_outside_media_box`, `box_not_normalized`, `box_degenerate`, `box_unreadable`, `rotation_not_quarter_turn` or `rotation_unreadable`; see ExplicitField), `field_out_of_bounds` (explicit placement: a field would not lie wholly on its page — `meta.reason` is `page_out_of_range`, `outside_page` or `empty_box`; from 2027-01-31 also `caption_outside_page`, a signature or initials box whose caption would cross the visible page's bottom edge). Both carry `meta.page` and `meta.reason` of the first problem and `meta.problems` = every problem, `[{ code, reason, page, field_index }]` (`field_indexes` for a page problem; `caption_depth_pt` and `space_below_pt` on `caption_outside_page`); nothing is created. Every 422 of ingest and placement carries the call's `warnings[]` (the notices of the request, as on the 201) when there are any.",
            "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; `meta.status` is its state now) or `expired` (`meta.status`, `meta.expires_at` — a reminder cannot reopen it).",
            "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…); `meta.status` is its state now. Also when it changed state during the call (no `meta`).",
            "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": "Stream the audit-trail PDF (Certificate of Completion)",
        "description": "The document's Certificate of Completion, issued once, when the last signer's seal completes the document, and stored: every request returns exactly the same bytes, and the completion emails attach that same file. Its SHA-256 is `audit_trail_sha256` on `document.completed`, `auditTrailSha256` on GET /documents/{id} and the `ETag` of this response. Always in English. It prints the SHA-256 of the signed PDF as completed (the `sha256` of `document.completed`, labelled \"Signed PDF SHA-256\"), the seal certificate serial and the time stamp, its times in the time zone of the signature captions (zone named on each time), the sender as \"created by\" (for a document created over the API: \"<workspace> · API key <name>\"), each signer's stated function and company (`job_title`, `company`) and the assurance level of each signature (electronic signature, or electronic signature with SMS 2FA), and the events up to the last signature; nothing recorded afterwards and no later change to the workspace alters it. It is not itself a sealed PDF: keep the copy you fetch and its hash. It prints masked IP addresses and no user agents. A document completed before 2026-10-15 gets its certificate at the first request after that date; a copy fetched earlier remains a valid record but is not byte-identical to it. 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 stays available after the workspace's retention period ends and `/signed` answers 410.",
        "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`; `ETag: \"<sha256>\"`.",
            "headers": {
              "ETag": {
                "description": "The lowercase hex SHA-256 of the body in double quotes — the same value as `audit_trail_sha256` on `document.completed` and `auditTrailSha256` on GET /documents/{id}.",
                "schema": {
                  "type": "string",
                  "examples": [
                    "\"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\""
                  ]
                }
              },
              "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` (`meta.signed`, `meta.total`, `meta.waiting`: 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": "`certificate_mismatch` (the stored certificate's bytes do not match their record — our failure, reported to us; retry later, and contact support if it persists) or `render_failed` (a certificate due at this request could not be issued). Retry with backoff.",
            "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, or the stored certificate could not be read or stored at that moment (it is never re-rendered in its place). 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 the copy presented for signing — prepared from your upload (`ingestNotes` on GET /documents/{id}), so not byte-identical to it — and deliberately not the sealed PDF, which `/signed` serves.",
        "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` (`meta.signed`, `meta.total`, `meta.waiting`: 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` (until 2026-12-31)",
                    "description": "Confirm takes no `field_values`. A reviewer corrects values on `review_url` before confirming. From 2026-12-31 (00:00 UTC) such a key is refused with 400 `unknown_field` instead.",
                    "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 — every problem with its field in `error` and in `meta.problems: [{ path, message }]` — or a body that is not valid JSON), `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\"] }]) `pinned-slot-mismatch` (a recipient override breaks a slot pin; `slot` names it) or `unknown_field` (from 2026-12-31 an unknown top-level key — `meta.fields` every refused key, `meta.accepted` what is accepted there).",
            "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, all in one response (it ended at the 1000th before revision 2026-10-20). 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"
          },
          "503": {
            "description": "`temporarily_unavailable` — a database read failed on our side, so the list cannot be given yet. Never final: retry after `Retry-After` seconds (5), then with backoff. (Before the 2026-10-20 release the same failure answered `200` with `templates: []`.)",
            "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"
                }
              }
            }
          }
        }
      }
    },
    "/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 (00:00 UTC) such a key is refused with 400 `unknown_field` — a JSON answer, no stream.",
                "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` (the body is not valid JSON — before 2026-09-24 it was read as `{}` and rendered the template unfilled; an empty body is still `{}` — or it failed the schema, every problem with its field in `error` and in `meta.problems: [{ path, 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`), or `unknown_field` (from 2026-12-31 an unknown top-level key, in every mode — the PDF stream answers this JSON instead — `meta.fields` every refused key, `meta.accepted` what is accepted there).",
            "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`, a `job_title` and their own `locale` 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",
                          "locale": "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",
                          "locale": "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.wesign.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",
                          "locale": "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",
                          "locale": "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` (the body is not valid JSON — before 2026-09-24 it was read as `{}` — or it failed the schema, every problem with its field in `error` and in `meta.problems: [{ path, 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, locale — 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), `duplicate_slot` (two or more recipients name the same `slot` — each recipient needs its own; `meta.slot`, `meta.emails`; since revision 2026-10-17), `pinned-slot-mismatch` (with `slot`: a recipient of a slot GET /templates/{id} publishes as `pinned: \"email\"` is not its `pinned_email`), `unknown_field` (from 2026-12-31 an unknown top-level key, `validate_only` included; an `Idempotency-Key` replay of a call made before that day still answers its original response — `meta.fields` every refused key, `meta.accepted` what is accepted there), 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.wesign.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.wesign.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**One origin per session.** The session is pinned to the `origin` you pass: the frame is served with `Content-Security-Policy: frame-ancestors <that origin>`, so the browser itself refuses to render it under any other parent, and it posts its messages to that origin only. Which origins a key may name is its list of allowed embed origins (Settings → Developers, or ask support): an empty list, the default, accepts any https origin; a non-empty list accepts only its members and refuses any other origin with 400 `origin_not_allowed`.\n\n**What your page's CSP needs.** One directive: `frame-src` with the origin the returned `embed_url` starts with — the workspace's signing subdomain, e.g. `https://<slug>.wesign.now`. 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 the key has a non-empty list of allowed embed origins and this origin is not on it; the message lists the origins that are. A key with an empty list (the default) accepts any https origin and never answers this.",
            "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"
          }
        ],
        "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"
          }
        ],
        "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"
          }
        ],
        "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"
          }
        ],
        "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"
          }
        ],
        "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"
          }
        ],
        "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 signer's language (en, de, fr, it, es or nl; see Locale — for a signer without a set language, the language their signing page showed). 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"
          }
        ],
        "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"
          }
        ],
        "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"
          }
        ],
        "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"
          }
        ],
        "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"
          }
        ],
        "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"
          }
        ],
        "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"
          }
        ],
        "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, created in the workspace's developer settings on every plan. New keys are `wsk_live_<32 hex>`; keys minted before September 2026 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 → Developers): 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 same rule as 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}$"
        }
      }
    },
    "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. `meta.reason` says which part to fix: `missing` (no Authorization header), `not_an_api_key` (a header, but not `Bearer wsk_live_…` / `lsk_live_…`) or `not_recognized` (unknown, revoked or out of scope — never which). 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 — the same rule as 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 → Developers.",
        "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 → Developers, 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 → Developers 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` — the body is not valid JSON, or failed the schema: every problem with its field in `error` (the first five) and in `meta.problems: [{ path, message }]` (all).",
        "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."
      },
      "UnknownEndpoint": {
        "description": "`unknown_endpoint` — no endpoint has this path (on api.* hosts also a path that left out `/v1`). Answered for every method, without authentication and without consuming the rate limit. `meta`: `method`, `path` (as called), `did_you_mean` (the closest endpoints as \"METHOD /v1/path\", best first; empty when nothing is close), `docs`. Before 2026-10-11 such a path answered an HTML page.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "No endpoint GET /v1/signing. Did you mean GET /v1/signing-requests/{id}, POST /v1/signing-requests or PATCH /v1/signing-requests/{id}? Every endpoint is listed at https://www.wesign.now/docs/api-overview.",
              "code": "unknown_endpoint",
              "meta": {
                "method": "GET",
                "path": "/v1/signing",
                "did_you_mean": [
                  "GET /v1/signing-requests/{id}",
                  "POST /v1/signing-requests",
                  "PATCH /v1/signing-requests/{id}"
                ],
                "docs": "https://www.wesign.now/docs/api-overview"
              }
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "`method_not_allowed` — the path is an endpoint, but not with this method. Without authentication and without consuming the rate limit. `meta`: `method`, `path` (as called), `endpoint` (the template), `allowed` (its methods), `docs`; the `Allow` header lists the same methods plus HEAD (with GET) and OPTIONS. Before 2026-10-11 the body was empty.",
        "headers": {
          "Allow": {
            "description": "The endpoint's methods, e.g. `OPTIONS, POST`.",
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "GET is not allowed on /v1/signing-requests/{id}/withdraw: it accepts POST /v1/signing-requests/{id}/withdraw. Every endpoint is listed at https://www.wesign.now/docs/api-overview.",
              "code": "method_not_allowed",
              "meta": {
                "method": "GET",
                "path": "/v1/signing-requests/0b9c7a1e-1111-4222-8333-444455556666/withdraw",
                "endpoint": "/v1/signing-requests/{id}/withdraw",
                "allowed": [
                  "POST"
                ],
                "docs": "https://www.wesign.now/docs/api-overview"
              }
            }
          }
        }
      }
    },
    "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 }` on `tier_required` and `api_monthly_limit`, `{ 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` (paths such as `fields[0].type` from 2027-01-31), `{ role, emails }` on `duplicate_role`, `{ slot, emails }` on `duplicate_slot`, `{ page, reason, problems }` on `field_out_of_bounds` and `page_geometry_unsupported`, `{ signatures: [ExistingSignature] }` on `pdf_already_signed`, `{ 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). `{ problems: [{ path, message }] }` on a schema `invalid_request`, `{ reason }` on `invalid_key`, `{ status }` on `invalid_state` (remind, withdraw), `{ status, expires_at }` on `expired`, `{ signed, total, waiting }` on `not_complete`, `{ method, path, did_you_mean, docs }` on `unknown_endpoint`, `{ method, path, endpoint, allowed, docs }` on `method_not_allowed`. (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."
          },
          "warnings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiWarning"
            },
            "description": "Notices of this call that were not the reason for the refusal, when there are any — on the 422s of ingest and placement of POST /signing-requests (`field_out_of_bounds`, `page_geometry_unsupported`, `placement_failed`, `pdf_user_password_required`, `pdf_decrypt_failed`, `pdf_already_signed`), and on a 400 `unknown_field` that refuses a top-level key while keys inside fields are still only named (until 2027-01-31)."
          }
        }
      },
      "Ok": {
        "type": "object",
        "required": [
          "ok"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          }
        }
      },
      "Locale": {
        "type": "string",
        "enum": [
          "en",
          "de",
          "fr",
          "it",
          "nl",
          "es"
        ],
        "description": "A language: `en`, `de`, `fr`, `it`, `nl` or `es`, the two-letter code only (`de-CH` is a 400 `invalid_request`).\n\nA signer's language decides their invitation email, reminders, the SMS code, the signing page and their copy of the signed document. Every `locale` is optional, and a call that names no language anywhere is never refused. For each signer the first of these applies:\n1. the signer's own `locale` (`signers[].locale` on POST /signing-requests, `recipients[].locale` on instantiate);\n2. the request's `locale`;\n3. the language remembered for this email address in your workspace: the language of the page the person last signed or declined on, else the last per-signer `locale` your workspace gave for them (a request-level `locale` is never remembered; another workspace never uses yours);\n4. your workspace's language (Settings → Signing; unset for every existing workspace until an owner sets it);\n5. English.\n\nSteps 1–3 count as a language you set. When step 4 or 5 applies, the email goes out in that language, but the signing link has no language segment (`…/sign/<token>`) and the signing page opens in the signer's browser language; once the signer's browser has loaded the document, reminders, the SMS code and their signed copy follow the language the page showed them.\n\nA signer whose own wesign.now account has a language set is always sent in that language. It is never shown: the responses, the read-backs, `signingUrl` and the webhooks carry the language from steps 1–5, so no call tells you whether an address has an account.\n\nThe caption under each signature, the audit certificate and the sealed PDF stay English. Since revision 2026-10-19; before, a request without `locale` was English and instantiate had no per-recipient `locale`."
      },
      "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 → Developers → Fields & API objects 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`, `audit_trail_sha256` on document.completed — sent with `User-Agent: wesign-webhooks/1`. 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` 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`, `audit_trail_sha256` on document.completed — sent with `User-Agent: wesign-webhooks/1`. 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. Unique per request: fields are assigned by role, so two signers with the same role are refused with 400 `duplicate_role` (e.g. use `signer1` and `signer2`)."
          },
          "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. In the object form, a key other than `en` / `de` is ignored and named in `warnings[]` (`signers[i].role_label.<key>`); 400 `unknown_field` from 2027-01-31.",
            "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",
            "description": "This signer's language: their invitation email, reminders, SMS code, signing page and signed copy. Optional; without it the request's `locale`, then the language remembered for this person in your workspace, then your workspace language, then English (Locale has the full rule). Remembered for this email address in your workspace. Read back as `locale` on this signer in the 201, on GET /signing-requests/{id} and GET /documents/{id}, and on `signing_request.sent`."
          },
          "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",
        "description": "One explicit field. `x`, `y`, `w` and `h` are ALWAYS fractions of the VISIBLE page: the CropBox (the MediaBox when the page has none) after `/Rotate` is applied, measured from its visible top-left corner (`origin: \"center\"` gives the field's centre instead). That is the page as a PDF viewer displays it, the page the signer sees, and the frame the signature is drawn in on the signed PDF. Portrait and landscape pages, `/Rotate` 90, 180 and 270, a MediaBox that does not start at 0/0 and a CropBox inside the MediaBox are all supported; the uploaded PDF is never rotated or cropped to make a field fit. A page wesign cannot place on exactly that way is refused with 422 `page_geometry_unsupported`: a CropBox not wholly inside the MediaBox, a box written reversed or under 1 pt on a side, a MediaBox or CropBox that is missing or not four numbers, or a `/Rotate` that is not a number or not a multiple of 90. A field that would not lie wholly on its page is refused with 422 `field_out_of_bounds`: a `page` the document does not have, `x + w > 1`, `y + h > 1`, `w` or `h` of 0, or an `origin: \"center\"` field reaching past an edge. Never placed approximately. Both are checked on the PDF as wesign presents it for signing, before anything is created, and only on pages that carry a field; `meta.problems` lists every offending page and field. Every signature and initials box carries a caption below it: 42 pt deep, 8.5 pt more for each extra line a stated `job_title` / `company` takes. Keep `y + h ≤ 1 − D / H` (H = visible page height in points; 0.950 on A4 portrait for D = 42). A box whose caption would cross the bottom edge is named in `warnings[]` (`caption_outside_page`) and refused with 422 `field_out_of_bounds` (reason `caption_outside_page`) from 2027-01-31; a field inside the D pt under a signature or initials box, at least across its width, is named (`field_over_caption`). A caption whose widest line would run past the right edge moves left (never cut there). A field accepts exactly these keys. Any other key is ignored and named in `warnings[]` (`unknown_field`, `fields[i].<key>`); from 2027-01-31 it is refused with 400 `unknown_field`.",
        "required": [
          "page",
          "x",
          "y",
          "w",
          "h",
          "role"
        ],
        "properties": {
          "page": {
            "type": "integer",
            "minimum": 0,
            "description": "0-based page index; must exist in the document (else 422 `field_out_of_bounds`, reason `page_out_of_range`)."
          },
          "x": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Left edge as a fraction of the visible page's width (CropBox after `/Rotate`), from its visible left edge."
          },
          "y": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Top edge as a fraction of the visible page's height (CropBox after `/Rotate`), from its visible top edge."
          },
          "w": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Width as a fraction of the visible page's width; > 0, and `x + w` ≤ 1."
          },
          "h": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Height as a fraction of the visible page's height; > 0, and `y + h` ≤ 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 to top-left; not clamped — a centred field reaching past an edge is 422 `field_out_of_bounds`)."
          }
        }
      },
      "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",
            "description": "The language of every signer without their own `locale`, and of the observer emails. Optional, with no default: without it a signer gets the language remembered for them in your workspace, else your workspace language, else English, and observers your workspace language, else English (Locale has the full rule). A default for this request only: it is not remembered for anyone. Before revision 2026-10-19 it defaulted to `en`."
          },
          "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 stored for the signers, in total (each belongs to exactly one signer)."
              }
            }
          },
          "signers": {
            "type": "array",
            "items": {
              "allOf": [
                {
                  "type": "object",
                  "required": [
                    "role",
                    "email",
                    "status",
                    "signingUrl",
                    "signingRequestId",
                    "emailed"
                  ],
                  "properties": {
                    "role": {
                      "type": "string"
                    },
                    "email": {
                      "type": "string",
                      "format": "email"
                    },
                    "locale": {
                      "$ref": "#/components/schemas/Locale",
                      "description": "The language requested for this signer (Locale): their own `locale`, else the request's, else the one remembered for them in your workspace, else your workspace language, else English. Never the language of the signer's own wesign.now account, which only decides what they are sent in. Added in revision 2026-10-19; an `Idempotency-Key` replay of a call made before it returns the stored body, without `locale`."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "pending",
                        "queued"
                      ]
                    },
                    "signingUrl": {
                      "type": "string",
                      "format": "uri",
                      "description": "The signer's personal link: `https://<slug>.wesign.now/<locale>/sign/<token>` on a branded workspace (Branded, Teams, Enterprise), else `https://www.wesign.now/<locale>/sign/<token>`, in the language of `locale`. When no language was set for this signer (only your workspace language or English applied; see Locale), the link has no language segment (`https://<slug>.wesign.now/sign/<token>`) and the signing page opens in the signer's browser language. The invitation email carries the same link; only its language segment can differ, for a signer whose own account has a language set. 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": "Notices about this request: unknown keys that were ignored (a top-level key — a JSON key or a multipart field other than `file` — or one inside a field or `role_label` by its path), captions that would be cut or covered, a digital signature in the upload that preparing it removes. 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"
            ],
            "description": "The language requested for this signer: the same value as `locale` on the create's response (Locale: their own `locale`, else the request's, else the remembered one, else your workspace language, else English). It stays the same after the signer opens the document, and never shows the language of the signer's own wesign.now account, which only decides what they are sent in."
          },
          "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. The same link as on the create's response: no language segment when no language was set for this signer (Locale)."
          },
          "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` (`meta.cert_serial`, `meta.tsa_provider`; from 2026-10-15 also `meta.seal_key_version`, `meta.seal_profile` — `adbe.pkcs7.detached` or `ETSI.CAdES.detached` —, `meta.seal_timestamp` `{ provider, signed_at }` of the time-stamp embedded in a PAdES B-T seal, else null, and `meta.seal_timestamp_error`, null or why a B-T seal carries none: `tsa_unavailable`, `token_unusable`, `token_too_large`, `not_requested`), `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_created` (the request was created by POST /signing-requests — its first event; `meta.source` `v1_api`, `meta.api_key_id`, `meta.key_name`, null when unreadable, and `meta.existing_signatures` when the upload carried digital signatures), `api_instantiated`, `instance_staged`, `instance_confirmed`. The masked IP address and user agent of a view, a decline or a signature are recorded with the event, not returned in `meta`; the audit certificate (GET /documents/{id}/audit-trail) prints the masked IP address, never the user agent. 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",
          "auditTrailSha256",
          "ingestNotes",
          "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 — the copy we prepared from your upload (see `ingestNotes`), never the hash of the bytes you uploaded, and 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"
          },
          "auditTrailSha256": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{64}$",
            "description": "SHA-256 of the stored Certificate of Completion (GET /documents/{id}/audit-trail) once one is recorded — the `audit_trail_sha256` of `document.completed` and the `ETag` of the download; else null (not completed yet, or no certificate issued yet — a document completed before 2026-10-15 gets one at its first audit-trail download)."
          },
          "ingestNotes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "decrypted",
                "repaired",
                "form-baked",
                "flattened",
                "compressed",
                "signature-removed"
              ]
            },
            "description": "What we did to the uploaded file before it was presented for signing: `decrypted` (a password / permissions layer removed), `repaired` (re-written by our PDF engine so every viewer reads the pages the same way: drawing instructions re-serialised, text, fonts and images unchanged — done to almost every file), `form-baked` (fillable form fields baked into the page), `signature-removed` (an existing digital signature, see `existing_signature_removed`), and for files uploaded in the app `flattened` / `compressed`. `[]` for a clean upload, and for API-created documents made before 2026-10-15 (nothing was recorded). Values may be added."
          },
          "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"
            ],
            "description": "The language requested for this signer: the same value as `locale` on the create's response (Locale: their own `locale`, else the request's, else the remembered one, else your workspace language, else English). It stays the same after the signer opens the document, and never shows the language of the signer's own wesign.now account, which only decides what they are sent in."
          },
          "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. Otherwise the same link as on the create's response: no language segment when no language was set for this signer (Locale)."
          }
        }
      },
      "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, where your workspace language comes next, 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 signer's language (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 → Developers page in the request's `locale`, else English (`<app origin>/<locale>/settings/api`; the web app's origin, 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",
            "description": "Default `en`. The language of `review_url` and of the `docs` link of a 422, and on a plain generate (no `review`) the fallback language of a written-out date. Generate has no signers, so the signer rule of Locale does not apply."
          }
        }
      },
      "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), each slot at most once (400 `duplicate_slot`).",
            "items": {
              "type": "object",
              "required": [
                "slot",
                "email"
              ],
              "properties": {
                "slot": {
                  "type": "integer",
                  "minimum": 1,
                  "maximum": 20,
                  "description": "Unique per request: the slot's fields are assigned to the recipient that names it, so two recipients on one slot are refused with 400 `duplicate_slot`."
                },
                "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"
                },
                "locale": {
                  "$ref": "#/components/schemas/Locale",
                  "description": "This recipient's language, exactly as `locale` on Signer: their invitation email, reminders, SMS code, signing page and signed copy. Optional; without it the request's `locale`, then the language remembered for this person in your workspace, then your workspace language, then English (Locale). Remembered for this email address in your workspace. A `review: true` instance keeps it; confirm takes no `locale`. Added in revision 2026-10-19."
                }
              },
              "additionalProperties": false,
              "description": "Strict: a key other than slot, email, name, phone_e164, require_sms_verification, sms_gate, reference, company, job_title, locale answers 400 `unknown_recipient_field` (it is never dropped silently). The SMS fields, `reference`, `company`, `job_title` and `locale` 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",
            "description": "The language of every recipient without their own `locale`, as on POST /signing-requests (Locale has the full rule). Optional, with no default, and not remembered for anyone. It also names the language of `review_url` and spells a written-out date that names no language (for both: else your workspace language, else English), and the language of the `docs` link of a 422 (else English). Before revision 2026-10-19 it defaulted to `en`."
          },
          "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"
                    },
                    "locale": {
                      "$ref": "#/components/schemas/Locale",
                      "description": "The language requested for this signer (Locale): their own `locale`, else the request's, else the one remembered for them in your workspace, else your workspace language, else English. Never the language of the signer's own wesign.now account, which only decides what they are sent in. Added in revision 2026-10-19."
                    },
                    "signing_request_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "signing_url": {
                      "type": "string",
                      "format": "uri",
                      "description": "The recipient's personal link, built as `signingUrl` on POST /signing-requests: in the language of `locale`, or without a language segment when no language was set for this recipient (Locale). Contains their credential."
                    },
                    "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` (the web app's origin, 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"
                    },
                    "locale": {
                      "$ref": "#/components/schemas/Locale",
                      "description": "The language requested for this signer (Locale): their own `locale`, else the request's, else the one remembered for them in your workspace, else your workspace language, else English. Never the language of the signer's own wesign.now account, which only decides what they are sent in. Added in revision 2026-10-19."
                    },
                    "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",
                      "description": "The recipient's personal link, built as `signingUrl` on POST /signing-requests: in the language of their `locale`, or without a language segment when no language was set for this recipient (Locale). Contains their credential."
                    },
                    "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. When the key has a non-empty list of allowed embed origins it must be on that list; a key with an empty list (the default) accepts any https origin. 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",
            "description": "The frame's language: labels, buttons, the consent statement, the SMS step and every error state. Optional; without it the signer's `locale` as the API reads it back (the requested language of Locale: English when none was set and your workspace has no language). The frame does not follow the signer's browser, so pass your page's language when you know it. The PDF is never translated, and the audit trail stays English."
          },
          "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 (e.g. `https://<slug>.wesign.now`), which is the origin your `frame-src` must name.",
            "examples": [
              "https://acme.wesign.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",
                "description": "The language requested for this signer, as `locale` on the create's response (Locale), never their own account language."
              },
              "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 (Certificate of Completion), issued once at completion — its SHA-256 is `audit_trail_sha256`; 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."
              },
              "audit_trail_sha256": {
                "type": [
                  "string",
                  "null"
                ],
                "pattern": "^[0-9a-f]{64}$",
                "description": "Lowercase hex SHA-256 of the certificate at `audit_trail_url`, issued once at completion — every fetch returns those exact bytes (also the `ETag` of the download). Null when it could not be issued at that moment (the first download issues it); absent on events queued before 2026-10-15."
              },
              "cert_serial": {
                "type": "string",
                "description": "Serial of the seal certificate that made the last seal. From 2026-10-22, 00:00 UTC, new seals are made with a new certificate, `CN=wesign.now Signing Authority` (serial `6a5258df714ab427831c680e605d8231`); seals made before carry `0062c9cfa7868e51c7ed510fece02d96` (the previous seal certificate). For a document signed across that date this is the new serial (the last seal's); each signer's `signed` audit event (`auditEvents[].meta.cert_serial` on GET /signing-requests/{id}) names the certificate that sealed that signature. Every certificate, with its SHA-256 fingerprint: https://www.wesign.now/seal-certificates.json."
              },
              "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 key this endpoint does not know was ignored — a top-level key (`send_email`; refused with 400 `unknown_field` from 2026-12-31, 00:00 UTC), or, on POST /signing-requests, a key inside an explicit field or a signer's `role_label` object, named by its path (`fields[0].type`, `signers[0].role_label.fr`; refused from 2027-01-31, 00:00 UTC). At most 100 nested keys per list are named; one more entry (`field` `fields[]` / `signers[].role_label`) counts the rest. `deprecated_field`: a deprecated spelling was accepted (confirm `recipients[i].phone`, accepted until 2026-12-31). `caption_outside_page`: the caption printed under a signature or initials box would cross the bottom edge of the visible page (explicit: refused with 422 `field_out_of_bounds` from 2027-01-31; anchors: never refused). `field_over_caption`: a field lies inside such a caption's area and prints over it; never refused. `existing_signature_removed`: the uploaded PDF already carried a digital signature (`field` is `file` or `file_url`); preparing the file for signing breaks it and the seal removes it, only its visible appearance stays on the page — refused with 422 `pdf_already_signed` from 2027-01-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 body key or path vs. a template field key). Codes may be added; treat an unknown code as a notice.",
        "required": [
          "field",
          "code",
          "message"
        ],
        "properties": {
          "field": {
            "type": "string",
            "description": "The top-level key (`send_email`); the path of a key or field (`fields[0].type`, `signers[0].role_label.fr`, `fields[0]`, `recipients[0].phone`); `fields[]` / `signers[].role_label` for the count of keys beyond the first 100; an anchored field's marker as the PDF spells it (`[[ls:signature:tenant]]`); or `file` / `file_url` for a notice about the uploaded PDF."
          },
          "code": {
            "type": "string",
            "enum": [
              "unknown_field",
              "deprecated_field",
              "caption_outside_page",
              "field_over_caption",
              "existing_signature_removed"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "ExistingSignature": {
        "type": "object",
        "description": "A digital signature the uploaded PDF already carried — an entry of `meta.signatures` on 422 `pdf_already_signed` (from 2027-01-31). At most 50 are listed.",
        "required": [
          "field_name",
          "page",
          "sub_filter",
          "signed_at",
          "certification"
        ],
        "properties": {
          "field_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The signature field's full name (`Signature1`, `parties.client`); null when unnamed."
          },
          "page": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "0-based page of its widget; null when no page carries one."
          },
          "sub_filter": {
            "type": [
              "string",
              "null"
            ],
            "description": "The signature's /SubFilter, e.g. `adbe.pkcs7.detached`, `ETSI.CAdES.detached`, `ETSI.RFC3161` (a document time stamp)."
          },
          "signed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "The signing time the signature states (/M), as written in the file — not verified; null when absent or unreadable."
          },
          "certification": {
            "type": "boolean",
            "description": "A certification signature (the document's /Perms /DocMDP points at it)."
          }
        }
      },
      "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, not covered by `sha256`), issued once at completion: every fetch returns the same bytes, whose SHA-256 is `audit_trail_sha256`; never 410, it stays available after retention."
          },
          "audit_trail_sha256": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[0-9a-f]{64}$",
            "description": "Lowercase hex SHA-256 of the certificate at `audit_trail_url` (a hash, no personal data). Null when it could not be issued at that moment (the first download issues it); absent on events queued before 2026-10-15."
          }
        }
      },
      "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"
  }
}
