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" }
}'recipientsis one{ slot, email, name }per slot; a slot without one is400 missing_recipients.job_titleandcompany(optional) say on whose behalf each representative signs. They print under each signature asAnna Muster · Geschäftsführerin · Muster AG · anna@example.chand on the audit certificate (emailed and fromGET /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": truefirst when the data comes from a person. - Read
warningson the200: anunknown_fieldthere is a misspelt key, and its value was not printed. - The same
Idempotency-Keywith the same body within 24 hours replays the200; 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.
| Event | Means |
|---|---|
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.completed | Done: 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.
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 bydocumentIdfrom the201). - 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(orapi.letssign.nowif you deliberately use the alias); thesigned_pdf_urlfield 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:
- Trigger: form submission.
- Action 1: pull the PDF (Google Drive download).
- Action 2: POST to
https://api.wesign.now/v1/signing-requestswith a multipart body. Zapier's Webhooks step supports binary uploads via the Custom Request advanced mode. - 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 receive400 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.
- Webhook from CRM fires when stage flips to "Closed-Won".
- Your handler builds the PDF (template + opportunity field values).
- POST to
https://api.wesign.now/v1/signing-requestswithcallback_urlpointing back at your handler. - On
signing_request.signed, update the CRM custom fieldsigned_at. Ondocument.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-requestsandPOST /v1/templates/{id}/instantiate./reminddoes 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.signedand 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}/signedon every dashboard view; pull once ondocument.completedand store.
