Recipes

End-to-end integration patterns — Portant, 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. Re-running the same template against the same row replays cleanly. 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.

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.

Most no-code platforms throttle long-running steps — the multipart upload may take >10s for large PDFs. The endpoint is idempotent under the same Idempotency-Key, so a retry from Zapier's built-in error handler is safe.

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.)

Building your own

Three rules of thumb:

  • Always pass an Idempotency-Key on the create callsPOST /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.