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 answer 402 enterprise-required on 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. 1. Sign up free.
  2. 2. Open Settings → API and switch Developer settings on.
  3. 3. Settings → API → Create API key.
  4. 4. Call GET /v1/me with 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 here

The 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.signed

Verify 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/signed

Returns 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: false on POST /v1/signing-requests or instantiate — 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: true on instantiate and generate — the full check (on instantiate the SMS allowance included) and nothing is created. See Templates → dry run.
  • review: true on instantiate (or generate) — the document is built and staged, nothing is sent. Look at it at the review_url, then throw it away with POST /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.