letssign.now docs

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.

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.letssign.now/v1/signing-requests \
  -H "Authorization: Bearer $LSK_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"}
      ]' \
  -F 'callback_url=https://yourapp.com/letssign/callback'

Use your source-document ID as the Idempotency-Key. Re-running the same template against the same row replays cleanly.

Receive the signed PDF

Verify the webhook signature (see Webhooks), pull signed_pdf_url and audit_trail_url, archive both into your DMS.

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 letssign.now with 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.

Use placement="manual" to keep a human in the loop:

{
  "placement": "manual",
  "placement_assignee_email": "lawyer@yourfirm.com",
  "signers": [{ "email": "client@example.com", "role": "client" }]
}

The lawyer gets a placement URL, opens it (no login needed if the URL is from a trusted email), drops the signature field where it should go, and hits Send. Only THEN are the signing requests created. That avoids the failure mode of an LLM emailing a contract with the signature box on page 3 paragraph 4.

For agents that DO get a human review beforehand, placement="anchors"

  • explicit role-tagged anchors in the template is faster.

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 letssign.now 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-LetsSign-Event-Id is your dedup key on the receiver side — at- least-once delivery means retries occasionally re-fire the same event.

Building your own

Three rules of thumb:

  • Always pass an Idempotency-Key. Even on the cheap-looking /remind endpoint. Cheap operations are the ones most often retried.
  • 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.
  • 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.