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:
- Trigger: form submission.
- Action 1: pull the PDF (Google Drive download).
- Action 2: POST to letssign.now with 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.
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.
- Webhook from CRM fires when stage flips to "Closed-Won".
- Your handler builds the PDF (template + opportunity field values).
- POST to letssign.now with
callback_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-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
/remindendpoint. Cheap operations are the ones most often retried. - 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. - Cache the signed PDF locally. Don't re-fetch from
/v1/documents/{id}/signedon every dashboard view; pull once ondocument.completedand store.
