Quickstart
From zero to a signed PDF in under five minutes — terminal-only, no SDK.
This guide goes deeper than the home page summary. By the end you'll have downloaded a real PAdES-sealed PDF, seen a webhook fire against your endpoint, and you'll know which knobs to turn for production.
Every URL below uses the API host https://api.wesign.now/v1 — see
Base URL for what is and isn't the contract.
Prerequisites
-
A workspace on any plan. An API key and webhooks come with every plan, with a monthly cap on the documents you create through the API:
- Free: 10 documents a month, the plan's own cap
- Branded: 100 documents a month through the API
- Enterprise: no cap
Only a workspace owner or admin turns developer settings on and creates keys. Sending from templates (
instantiate,generate,confirm) and minting an embedded signing session need Enterprise and answer402 enterprise-requiredon other plans; listing and reading templates (GET /v1/templates,GET /v1/templates/{id}) works with any full-access key. -
A test PDF on disk. Any PDF works; if it doesn't have an anchor marker we just append a signature page.
-
An email address you can check (or two — sender + signer can be the same address while you smoke-test).
Sending from a template instead of a PDF? The Template quickstart walks through that flow.
1 · Get your API key
- 1. Sign up free.
- 2. Open Settings → API and switch Developer settings on.
- 3. Settings → API → Create API key.
- 4. Call
GET /v1/mewith the key. It sends nothing and shows the key works.
Copy the wsk_live_… plaintext into WESIGN_API_KEY — the same name the
.env download in the reveal dialog uses, and the name every sample on
this page reads:
export WESIGN_API_KEY="wsk_live_…" # paste the whole key hereThe plaintext shows once. The dashboard keeps a wsk_live_a1b… prefix
so you can identify the key later. A key minted before September 2026
starts with lsk_live_ and works exactly the same — no need to re-mint.
2 · Check the key: a call that sends nothing
GET /v1/me creates nothing, emails no one and takes no token from your
rate limit. A 200 proves the key works and names its
workspace, with what that workspace may use (capabilities) and its SMS
allowance; a 401 invalid_key says in meta.reason what to fix
(Authentication).
# Paste the full key — the dashboard only shows its prefix
export WESIGN_API_KEY="wsk_live_…"
# Sends nothing: checks the key and names its workspace
curl https://api.wesign.now/v1/me \
-H "Authorization: Bearer $WESIGN_API_KEY"
# 200 → { "workspace": { … }, "key": { … }, "capabilities": { … }, … }3 · Send a real request
Two signers with distinct roles, both named — the role is how a
signature field finds its signer, so two signers must never share one.
placement=auto_append adds a signature page, so the call works with any
PDF. Settings → API in the app shows the same sample for your workspace, with your
key's prefix and your own address.
# Paste the full key — the dashboard only shows its prefix
export WESIGN_API_KEY="wsk_live_…"
curl -X POST https://api.wesign.now/v1/signing-requests \
-H "Authorization: Bearer $WESIGN_API_KEY" \
-H "Idempotency-Key: quickstart-$(date +%s)" \
-F "file=@contract.pdf" \
-F "placement=auto_append" \
-F 'signers=[
{ "email": "anna.muster@example.com",
"name": "Anna Muster", "role": "counterparty" },
{ "email": "you@example.com",
"name": "Your name", "role": "company" }
]'
# 201 → { "documentId": "…", "signers": [
# { "role": "counterparty", "signingUrl": "…", "emailed": true },
# { "role": "company", "signingUrl": "…", "emailed": true } ] }
# Every answer carries an X-Request-Id header: quote it to
# support@wesign.now when something is off.Successful response (HTTP 201):
{
"documentId": "8a1e4f9a-b3c1-4d22-9e0a-3a92ee7c1f88",
"metadata": null,
"status": "pending",
"placement": "auto_append",
"presentedSha256": "4f9a…d21c",
"anchors": {
"found": 0,
"fields": 4
},
"signers": [
{
"role": "counterparty",
"email": "anna.muster@example.com",
"locale": "en",
"status": "pending",
"signingUrl": "https://yourco.wesign.now/sign/abc…",
"signingRequestId": "11111111-…",
"emailed": true,
"require_sms_verification": false,
"phone_masked": null,
"sms_verified_at": null,
"sms_gate": "before_sign",
"signature_level": "SES",
"reference": null,
"company": null,
"job_title": null
},
{
"role": "company",
"email": "you@example.com",
"locale": "en",
"status": "pending",
"signingUrl": "https://yourco.wesign.now/sign/def…",
"signingRequestId": "22222222-…",
"emailed": true,
"require_sms_verification": false,
"phone_masked": null,
"sms_verified_at": null,
"sms_gate": "before_sign",
"signature_level": "SES",
"reference": null,
"company": null,
"job_title": null
}
],
"observers": [],
"warnings": []
}"placement": "auto_append" with anchors.found: 0 means the PDF had no
anchor markers, so a signature page was appended — one block per role.
With placement=anchors you would instead drop [[ls:signature:counterparty]]
and [[ls:signature:company]] into the PDF where each signature belongs
(see Placement modes). One role per signer: two
signers sharing a role share one set of fields, and the second finds
nothing to sign.
emailed: true says we sent the invitation; the email carries the same
link as signingUrl. It is false when the send failed, or when the
address is on a domain that accepts no email (we do not send what can only
bounce). Open each signingUrl in another browser tab to play the
signer side — the counterparty's and your own. To deliver the link yourself instead, send
-F 'send_emails=false': everything else is created as usual and
emailed comes back false.
The call sent no locale, so locale is your workspace language, else en,
and signingUrl has no language segment: the signing page opens in the
signer's browser language. Add "locale": "de" to a signer to send them in
German — see Language per signer.
4 · Watch a webhook fire (optional)
Skip if you'd rather poll. To see the events live, the easiest path is a free Webhook.site URL:
# webhook.site/<your-token> issued at https://webhook.site
curl -X POST https://api.wesign.now/v1/signing-requests \
-H "Authorization: Bearer $WESIGN_API_KEY" \
-F "file=@contract.pdf" \
-F "placement=auto_append" \
-F 'signers=[
{ "email": "anna.muster@example.com",
"name": "Anna Muster", "role": "counterparty" },
{ "email": "you@example.com",
"name": "Your name", "role": "company" }
]' \
-F 'callback_url=https://webhook.site/YOUR-TOKEN'The response now includes a callback.secret — store it. Each event
we POST to your URL is signed:
X-WeSign-Signature: t=1757404800,v1=9c3b…a2f1
X-WeSign-Event-Id: evt_…
X-WeSign-Event: signing_request.signedVerify the signature before trusting the body.
5 · Sign
Open signingUrl on your phone (real test) or in another browser
tab (lazy test). Sign. Watch the inbox for the completion email and
your webhook receiver for signing_request.signed →
document.completed.
6 · Pull the signed PDF programmatically
DOC_ID=8a1e4f9a-b3c1-4d22-9e0a-3a92ee7c1f88 # the documentId from step 3
curl -LOJ -H "Authorization: Bearer $WESIGN_API_KEY" \
https://api.wesign.now/v1/documents/$DOC_ID/signedReturns the sealed PDF, ready for your DMS: every signer's signature on
the page under one PAdES seal. The RFC 3161 time stamp is kept with our
records (tsa_provider and tsa_signed_at on document.completed, and on
the audit certificate), not embedded in the PDF until the
seal-certificate change. This is the
same URL document.completed hands you as signed_pdf_url: it needs
your key, so pin that fetch to api.wesign.now and send the bearer —
see signed_pdf_url.
In production, fetch it on document.completed, compare the event's
sha256 and store it on your side: a workspace can have signed PDFs
deleted after a set number of days. The whole flow is in
Retrieve the signed document.
Testing without a sandbox
There are no test keys yet: every key is live, every call creates real documents and sends real email and SMS, and SMS codes count against the monthly allowance. Three switches keep a test contained:
send_emails: falseonPOST /v1/signing-requestsorinstantiate— the document and the signing links are created and returned, no invitation is emailed, not even to the next signer of a sequential send when their turn comes. Open the links yourself. Automatic reminders (7 and 14 days after the send, by default) and the completion emails still go out, so use addresses you own, and withdraw what you leave unsigned.validate_only: trueoninstantiateandgenerate— the full check (oninstantiatethe SMS allowance included) and nothing is created. See Templates → dry run.review: trueoninstantiate(orgenerate) — the document is built and staged, nothing is sent. Look at it at thereview_url, then throw it away withPOST /v1/documents/{id}/discard. See Review before sending.
A sandbox with simulated email and SMS is planned — see Versioning → No sandbox yet.
What's next
- Template quickstart — send the same contract many times from a locked template.
- Placement modes — pick anchors, auto-append, or explicit coordinates.
- Second factor by SMS — require a one-time code on the signer's phone before they can sign.
- Webhooks — production-grade event delivery, retries, signature verification.
- Idempotency — safely retry network blips.
- Errors — full code table.
API documentation
Send a PDF or a filled template for signature over one REST API. The wesign.now API reference, guides, errors and support.
Template quickstart
From zero to a sealed PDF made from a template. A one-signer rich-text letter, or a two-signer PDF your system fills and two people sign in order.
