Recipes

End-to-end integration patterns — Portant, a two-signer template, Zapier, agent-generated PDFs, CRM sync.

Common shapes integrators ship. Each is a real call sequence with the corner cases called out. Every URL is on the canonical host https://api.wesign.now/v1; the api.letssign.now alias works identically.

Document automation tool → letssign.now

Pattern: Your Portant / DocAssemble / Pandoc / LaTeX pipeline generates the PDF; letssign.now sends it for signature.

Embed anchors at template authoring time

Drop [[ls:signature:role]] tokens in the template:

<!-- HTML→PDF template -->
<p>Tenant signature: [[ls:signature:tenant]]</p>
<p>Date: [[ls:date:tenant]]</p>

The role is your business-domain identifier. Pick something stable; you'll reuse it as signers[].role.

Render the PDF

Whatever your generator is, write the bytes to disk or memory. Don't flatten — the marker needs to remain in the text layer.

POST to the endpoint

curl -X POST https://api.wesign.now/v1/signing-requests \
  -H "Authorization: Bearer $WSK_KEY" \
  -H "Idempotency-Key: ${SOURCE_DOC_ID}" \
  -F "file=@/tmp/contract.pdf" \
  -F 'signers=[
        {"email":"tenant@example.com",  "role":"tenant"},
        {"email":"landlord@example.com","role":"landlord", "phone_e164":"+41791112233", "require_sms_verification":true}
      ]' \
  -F 'callback_url=https://yourapp.com/wesign/callback'

Use your source-document ID as the Idempotency-Key. A retry with the same key and the same bytes and fields within 24 hours replays the first 201 instead of sending again. Retry with the bytes you already sent, not a fresh render: most generators stamp a creation time into the PDF, so a re-render under the same key is 422 idempotency_key_reuse. Details in Idempotency. The landlord above has to confirm an SMS code before signing — see Second factor by SMS.

Receive the signed PDF

Verify the webhook signature (see Webhooks), then fetch signed_pdf_url with your API key — it is https://api.wesign.now/v1/documents/{id}/signed, so pin the fetch to api.wesign.now — and fetch audit_trail_url the same way (same host, same key: …/documents/{id}/audit-trail), then archive both into your DMS.

Two-signer power of attorney from a template

Pattern: A company grants a power of attorney that two representatives sign, one after the other. The contract is a locked PDF template in your workspace; your system supplies the company's details per case. Needs the Enterprise template entitlement (GET /v1/me → capabilities.templates).

Author the template once in the app, as in the PDF track of the Template quickstart: two recipient slots, company.legal_name and company.tax_id as Sender / API boxes, signing.place and signing.place_s2 as Signer boxes, a signature per slot, locked.

Read the contract when you build, not per case

GET /v1/templates/{id} answers the slots (recipients) and every input with its required flag. Build your mapping against that version and pin it with "version" on every call, so an author who edits the template later publishes a new version without changing yours.

While someone has the template unlocked for editing, every call answers 409 template_unlocked, pinned or not. Once it is locked again, a call pinned to the old version keeps that version's contract.

Send one call per case

curl -X POST https://api.wesign.now/v1/templates/$TPL_ID/instantiate \
  -H "Authorization: Bearer $WSK_KEY" \
  -H "Idempotency-Key: ZT-2026-0142-poa" \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": [
      { "slot": 1, "email": "anna@example.ch", "name": "Anna Muster",
        "job_title": "Geschäftsführerin", "company": "Muster AG" },
      { "slot": 2, "email": "ben@example.ch", "name": "Ben Keller",
        "job_title": "Mitglied des Verwaltungsrates", "company": "Muster AG" }
    ],
    "field_values": {
      "company.legal_name": "Muster AG",
      "company.tax_id": "DE123456789-00001"
    },
    "signing_mode": "sequential",
    "version": 3,
    "locale": "de",
    "metadata": { "case_id": "ZT-2026-0142" }
  }'
  • recipients is one { slot, email, name } per slot; a slot without one is 400 missing_recipients.
  • job_title and company (optional) say on whose behalf each representative signs. They print under each signature as Anna Muster · Geschäftsführerin · Muster AG · anna@example.ch and on the audit certificate (emailed and from GET /v1/documents/{id}/audit-trail), labelled "stated by the sender"; we do not verify them.
  • A missing Sender / API value is 422 template_input_invalid, with every problem listed at once. Run the same body with "validate_only": true first when the data comes from a person.
  • Read warnings on the 200: an unknown_field there is a misspelt key, and its value was not printed.
  • The same Idempotency-Key with the same body within 24 hours replays the 200; it never sends twice.

Both representatives see the company name and tax ID from their first view, as fixed text; the sealed PDF prints them once.

Follow the chain

Slot 1 is invited at once. Slot 2 stays queued until slot 1 has signed, then is released: invited by email automatically, or — with send_emails: false — not emailed, and you deliver slot 2's signing_url.

EventMeans
signing_request.sent ("source": "v1_api")Sent to slot 1. Carries your metadata.
signing_request.signed (slot 1's signing_request_id)Slot 1 signed.
signing_request.sent ("source": "sequential_release", signer.slot: 2)Slot 2 is released. emailed: false with send_emails: false: deliver its link now. May arrive before slot 1's signed.
signing_request.viewed (signer.slot: 2)Slot 2 opened the document.
signing_request.signed, then document.completedDone: fetch the files.

signing_request.signed names the signer by signing_request_id (keep both from the 200), not by slot. If slot 1 declines, or you withdraw it, the chain stops: slot 2 is never invited and the document never completes.

Archive

On document.completed, verify the signature (see Webhooks), then fetch signed_pdf_url and audit_trail_url with your key, pinned to api.wesign.now, and store both with the case.

Multi-tenant platform (one key, many clients)

Pattern: You run a platform for many firms and send on their behalf.

  • One workspace API key covers every document the workspace sends; hooks are per workspace, so one dashboard-registered webhook with one stable secret receives everything. Route on document_id (or on an id you keep in your own table keyed by documentId from the 201).
  • If a client needs its own brand on the signing page, that is a workspace of its own with its own key — brand, hosting region and reply-to all hang off the key.
  • Pin outbound fetches to api.wesign.now (or api.letssign.now if you deliberately use the alias); the signed_pdf_url field only ever names the canonical host.

Zapier / no-code triggers

Pattern: A Google Form / Typeform / Notion entry triggers a signing.

Without an SDK you can stitch this with Zapier's "Webhooks by Zapier" action:

  1. Trigger: form submission.
  2. Action 1: pull the PDF (Google Drive download).
  3. Action 2: POST to https://api.wesign.now/v1/signing-requests with a multipart body. Zapier's Webhooks step supports binary uploads via the Custom Request advanced mode.
  4. Action 3: log the response in a Google Sheet for audit.

If the PDF already sits at a URL we can fetch directly (https or http, no redirects, ≤ 25 MB), skip the binary step: send JSON with file_url instead of a file part.

Most no-code platforms throttle long-running steps — the multipart upload may take >10s for large PDFs. Set an Idempotency-Key header from the trigger (the form submission's id) and a retry from Zapier's built-in error handler replays the first response instead of sending twice. Without the header, every retry is a new send.

Agent-generated PDFs

Pattern: An AI agent (Claude tool-use, OpenAI function-calling, LangChain/LangGraph) produces a draft contract and ships it for signature without human-in-the-loop review.

Have the agent emit role-tagged anchors — [[ls:signature:client]] — into the draft and send with placement="anchors". The anchor is placed where the signature belongs by construction, so there is no "signature box on page 3 paragraph 4" failure mode. For fully programmatic layouts, placement="explicit" takes exact coordinates.

placement="manual" (a human-review placement URL) was retired on 2026-07-31 — requests using it receive 400 placement_retired. Keep the human in the loop before the send instead: generate, review, then dispatch with anchors — or stage a template instance for review.

CRM sync (Salesforce / HubSpot / Attio)

Pattern: A Closed-Won opportunity triggers a contract send; contract status updates back to the CRM record.

  1. Webhook from CRM fires when stage flips to "Closed-Won".
  2. Your handler builds the PDF (template + opportunity field values).
  3. POST to https://api.wesign.now/v1/signing-requests with callback_url pointing back at your handler.
  4. On signing_request.signed, update the CRM custom field signed_at. On document.completed, archive the signed PDF + the audit trail in the CRM Notes / Files tab.

X-WeSign-Event-Id is your dedup key on the receiver side — at- least-once delivery means retries occasionally re-fire the same event. (The same value also arrives as X-LetsSign-Event-Id.) For document.completed, deduplicate on document_id too: a database failure at the moment of completion can repeat it with a new id.

Building your own

Three rules of thumb:

  • Always pass an Idempotency-Key on the create calls — POST /v1/signing-requests and POST /v1/templates/{id}/instantiate. /remind does not take one and re-sends on every call, so debounce it yourself.
  • Verify webhook signatures. Skipping this is the #1 incident pattern; an attacker can forge signing_request.signed and your handler updates a CRM with bogus state. Node and PHP snippets are in Verifying a signature.
  • Cache the signed PDF locally. Don't re-fetch from /v1/documents/{id}/signed on every dashboard view; pull once on document.completed and store.