Embedded signing

White-label guest signing — your users place fields and sign inside your app, no letssign.now account required.

Embedded signing lets your app start a signing session for one of your end-users (a guest, identified by their email + your own user id). The guest is redirected to a no-login URL on your subdomain, places fields, and signs — or sends to others. The signature is partner-vouched (your app already authenticated them), with optional SMS step-up for AES-grade assurance.

Embedded signing is an Enterprise capability, gated by embedded_signing_enabled. Talk to sales to enable it for your workspace and get a sandbox key.

Create a session

Authenticate with your workspace API key. We upsert the guest, ingest the PDF (by URL or an existing document_id), and return a sessionUrl — redirect your user's browser there.

Request body

JSON (Content-Type: application/json).

FieldTypeRequiredDescription
guest{ external_user_id, email, name? }Your end-user. external_user_id is your stable id; one guest per (workspace, id).
signersSigner[]1–20. Each role: "self" (the guest) or "other" (external).
capability"place" | "send" | "sign"Default place.
document{ file_url? | document_id? }A direct PDF URL (≤ 25 MB) or an existing document in your workspace.
signing_mode"parallel" | "sequential"Default parallel.
locale"en" | "de"Default en.
expires_in_daysint 1..90Default 14.
redirect_urlstringWhere to return the user after signing.

Signer object

FieldTypeRequiredDescription
role"self" | "other"self = the guest signs in-app; other = an external signer gets a request.
emailstringSigner's email.
namestringDisplay name.
phone_e164stringRequired when require_sms is true.
require_smsbooleanSMS step-up → AES-grade for this signer.
signing_orderint ≥ 1Only when signing_mode="sequential".

Request

curl -X POST https://api.letssign.now/v1/embedded/sessions \
  -H "Authorization: Bearer $LSK_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "guest": { "external_user_id": "u_8123", "email": "ann@acme.com" },
    "capability": "place",
    "document": { "file_url": "https://files.acme.com/msa.pdf" },
    "signers": [
      { "role": "self",  "email": "ann@acme.com", "require_sms": true, "phone_e164": "+41791234567" },
      { "role": "other", "email": "legal@acme.com" }
    ],
    "redirect_url": "https://app.acme.com/contracts/8123/done"
  }'

Response

HTTP/1.1 201 Created
Content-Type: application/json

{
  "sessionId":  "9b2e…",
  "guestId":    "1f4a…",
  "capability": "place",
  "sessionUrl": "https://acme.letssign.now/en/place/7b1c…",
  "expiresAt":  "2026-07-01T10:30:00Z",
  "signers": [
    { "role": "self",  "email": "ann@acme.com" },
    { "role": "other", "email": "legal@acme.com" }
  ]
}

Redirect the browser to sessionUrl. The guest places the fields and hits Send — their own signature view opens if they're a self signer; other signers get their invite. Every signing request created is stamped with your API key + the session id for the audit trail.

sessionUrl is single-use and short-lived. The guest's identity is vouched by your app; set require_sms on a signer to step up to AES-grade verification. The assurance level is recorded on the audit trail.

Get session status

GET /v1/embedded/sessions/{id}
{
  "id":         "9b2e…",
  "status":     "active",
  "capability": "place",
  "guestId":    "1f4a…",
  "target":     { "kind": "document", "id": "8a1e…" },
  "expiresAt":  "2026-07-01T10:30:00Z",
  "createdAt":  "2026-06-17T10:30:00Z",
  "consumedAt": null,
  "revokedAt":  null
}

status: pendingactiveconsumed (the guest sent) · expired · revoked. Or subscribe to webhooks instead of polling.

Revoke a session

DELETE /v1/embedded/sessions/{id}

Kills the session and its sessionUrl (the link stops working). Idempotent; returns 409 if the session was already consumed.

200 OK    { "ok": true, "status": "revoked" }