Placement modes
Where signature fields land on the PDF — anchors, auto-append, explicit, manual. Visualised, with when-to-use guidance.
Pick a mode with the placement form field on
POST /v1/signing-requests. The
default is anchors with a fall-through to auto_append when no
markers are found. Four modes total, no fifth.
There is no separate "magic-link" mode. An earlier draft of this
API had placement_link_mode: "login_required" | "magic" as a
parameter on manual mode. It was collapsed before launch —
manual now handles both paths under a single URL: if the visitor
is signed into the matching workspace, the audit trail records
their user_id; otherwise the high-entropy single-use token in the
URL doubles as the credential. Same effective security, simpler
contract.
At a glance
| Mode | When to pick it | Caller passes coordinates? | Field placement happens at |
|---|---|---|---|
anchors | You generated the PDF programmatically (HTML→PDF, LaTeX, Word) with markers. | No | API call time |
auto_append | Ad-hoc letters, no structure, just need a signature page on the end. | No | API call time |
explicit | You generated the PDF programmatically AND tracked field positions yourself. | Yes | API call time |
manual | A human places fields after upload — paralegal review, external counsel. | No (deferred) | When the placer hits Send |
anchors (default)
Drop a marker like [[ls:signature:tenant]] in your PDF (HTML→PDF,
LaTeX, Word, anywhere with a real text layer). We find it, mask the
marker with a white rectangle, and place the signature field on top.
Tenant signature: [[ls:signature:tenant]] ─────────────────
Best ergonomics for programmatically generated contracts — HTML→PDF templates, document-assembly tools (Portant, DocAssemble), LaTeX, Pandoc. See Anchor placeholders for the full marker syntax.
Caveats:
- The PDF's text layer must contain the marker. Scanned-image PDFs don't qualify (run them through OCR first).
- Markers wrapped across two lines are skipped silently — keep them on one line in the source template.
- A marker can appear once per page per role for
signature/initial/date/name. Duplicates → 400duplicate_anchor.
auto_append
We add a fresh signature page after your PDF — one block per signer, role-labelled with a signature line and a date line. No coordinates needed from you.
Good for ad-hoc letters and quick-and-clean send-and-forget flows
where there's no existing signature area. Also the
fall-through when anchors mode finds zero markers in the
PDF — rather than bouncing your call with no_anchors_found, we
silently switch to auto_append so the document still goes out.
Caveats:
- Adds at least one extra page (more if you have >7 signers).
- Page size matches A4 regardless of your source PDF's size.
explicit
You pass a fields[] array with one tuple per field, in normalized
[0..1] coordinates with a top-left origin:
{
"placement": "explicit",
"fields": [
{ "page": 0, "x": 0.10, "y": 0.85, "w": 0.30, "h": 0.06, "kind": "signature", "role": "tenant" },
{ "page": 0, "x": 0.45, "y": 0.85, "w": 0.30, "h": 0.06, "kind": "signature", "role": "landlord" },
{ "page": 0, "x": 0.10, "y": 0.92, "w": 0.15, "h": 0.03, "kind": "date", "role": "tenant" }
]
}{ "page": 0,
"x": 0.10,
"y": 0.85,
...Use when you generated the PDF programmatically AND tracked field positions at generation time — most often a templating system with inline metadata about where each field belongs.
Coordinate system:
- Origin top-left (matches our editor; PDF native is bottom-left but you don't need to think about that).
- Both
xandyare in[0, 1]— fractions of page width / height. kindissignature|initial|date|text.roleMUST match asigners[].rolevalue.
Caveats:
- No safety net — coords land where you say. A bug in your generator puts the signature box on top of the legalese.
- Page sizes vary; if your generator outputs Letter and the field table assumes A4, fields land where they shouldn't.
manual
Returns a placementUrl instead of emailing signers immediately. The
recipient — typically a paralegal, legal counsel, or account manager —
opens the URL, drops fields in our editor, and hits Send. Only
THEN are the signing requests created and the signers emailed.
{ "placement":"manual",
"placement_assignee_email":"…" }{
"placement": "manual",
"placement_assignee_email": "paralegal@yourfirm.com",
"signers": [
{ "email": "tenant@example.com", "role": "tenant" },
{ "email": "landlord@example.com", "role": "landlord" }
]
}Response (HTTP 202):
{
"documentId": "8a1e4f9a-…",
"status": "awaiting_placement",
"placement": "manual",
"placementUrl": "https://letssign.now/place/<token>",
"placementToken": "abc…",
"placementAssigneeEmail": "paralegal@yourfirm.com",
"placementExpiresAt": "2026-05-13T10:30:00Z",
"signers": [
{ "role": "tenant", "email": "tenant@example.com", "status": "awaiting_placement" },
{ "role": "landlord", "email": "landlord@example.com", "status": "awaiting_placement" }
]
}The placement link works with or without a workspace login. If
the visitor is signed into the matching workspace, the audit trail
records their user_id; otherwise the token in the URL is the
credential and the trail records the assignee email. Same effective
security as a signed-in flow — token is 32 hex characters (128
bits), single-use, document-scoped, 14-day TTL.
Useful for:
- Legal review / paralegal placement — the API caller is your CRM trigger, the placer is a real human at your firm.
- External counsel — outside law firm reviewing a contract on their own computer with no workspace account.
- Agent-generated PDFs with human review — an LLM drafts the contract, a lawyer places the signature fields before send. Avoids the failure mode of an AI putting the signature box mid-paragraph.
Caveats:
- Two-step flow — the original POST creates a placeholder, not a live signing request. Until the placer hits Send, no signer is notified and no signing_request row exists.
- The placement token expires after 14 days (configurable via
expires_in_days). After that, the document is stranded — recover by re-POSTing. - Status
awaiting_placementis distinct in the GET /v1/documents rollup so your dashboard can show a "needs human" queue.
