· Developers & agents

API & MCP

Last updated · 2026-06-08


Sign Sail's API does the whole signing flow over HTTP: upload a PDF, drop signature, date, and text fields onto it, send it to one or more signers, watch their progress, and pull back the sealed PDF once everyone has signed. No dashboard step is required. Everything the builder UI does, a script or an agent can do through the API.

Authentication

Every request authenticates with an API key, passed as Authorization: Bearer ss_live_…. Keys are created under Account → Developer settings and shown once at creation, so store yours somewhere safe. API access is a Business-plan feature; keys on Free or Pro accounts return 403.

Built for AI agents

The API is meant to be driven by code you didn't hand-write. The full surface is described by an OpenAPI 3.1 spec at /api/v1/openapi.json, so an agent can read it and learn every endpoint, parameter, and response on its own. Errors carry a stable code field, not just a prose message, so retry logic can branch on them, and any POST accepts an Idempotency-Key header so a retried request never creates a duplicate. Prefer not to touch HTTP at all? Connect to the MCP server at /api/mcp. It exposes the same operations (upload, place fields, send, check status, download) as tools, authenticated with the same API key.

Drop-in agent skill

Rather than wire this up by hand, hand your agent the published SKILL.md. It teaches the whole flow — setup, field placement, the tag grammar, error codes, idempotency, and the MCP tools — so a Claude Code or similar agent can drive Sign Sail with no further prompting. Install it as a personal skill:

mkdir -p ~/.claude/skills/signsail && curl -sL https://signsail.com/skill.md -o ~/.claude/skills/signsail/SKILL.md

Any agent can also fetch the raw file at /skill.md; it is linked from /llms.txt so agents reading the site discover it automatically.

Quickstart

Here is the whole flow end to end: upload a PDF, place two fields for a single signer, send it, poll until it's done, then download the signed copy. Set KEY to your ss_live_ key first; every call reuses it.

# 1. Set your key (created under Account → Developer settings)
KEY="ss_live_…"
BASE="https://signsail.com/api/v1"

# 2. Upload a PDF by URL (or use -F [email protected] for multipart)
DOC=$(curl -s -X POST "$BASE/documents" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/contract.pdf","title":"NDA"}' | jq -r .id)

# 3. Place two fields for signer 1 (coords are normalized 0..1, top-left)
curl -s -X PUT "$BASE/documents/$DOC/fields" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"placements":[
        {"recipientRef":1,"fieldType":"SIGNATURE","pageIndex":0,
         "x":0.12,"y":0.80,"w":0.25,"h":0.06,"required":true},
        {"recipientRef":1,"fieldType":"DATE_SIGNED","pageIndex":0,
         "x":0.45,"y":0.80,"w":0.18,"h":0.04,"required":true}
      ]}'

# 4. Send it (Idempotency-Key makes the retry safe)
ENV=$(curl -s -X POST "$BASE/envelopes" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: nda-send-001" \
  -d "{\"documentId\":\"$DOC\",\"recipients\":[{\"email\":\"[email protected]\",\"name\":\"Dana\"}]}" | jq -r .id)

# 5. Poll status, then download both files once completed
curl -s "$BASE/envelopes/$ENV" -H "Authorization: Bearer $KEY" | jq .status
curl -s "$BASE/envelopes/$ENV/pdf" -H "Authorization: Bearer $KEY" -o signed.pdf
curl -s "$BASE/envelopes/$ENV/certificate" -H "Authorization: Bearer $KEY" -o certificate.pdf

Auto-place fields with tags

Instead of sending field coordinates, you can put the fields in the document itself. Write a tag like {{sig:s1}} where a signature goes, {{date}} where a date goes, and Sign Sail finds each tag on upload and places the field there, with no human dragging boxes and no coordinate math. The literal tag text is covered with a white box before any signer sees the document, so they get a clean page with just the fields.

Reading a PDF's text is CPU work, so detection runs in a background worker rather than blocking your upload. POST /documents returns status: "processing" right away; the document then settles to ready (fields placed) or draft (no tags found). Poll GET /documents/{id}/fields until the status changes, or subscribe to the document.processed webhook and skip polling. Uploads stay fast even when a lot of customers upload at the same time.

Auto-placement never sends anything. The document is sendable once it's ready, and your explicit POST /envelopes is the approval step. Every auto-placed field carries source: "AUTO", and the document's lastDetectionlists any warnings, such as a tag that couldn't be parsed. Review them in the dashboard, or have your agent gate on them. For example, only auto-send when warnings is empty.

Tag grammar: {{ type[:sN][:opt][:w=pt][:h=pt] }}. Each field type has a default size; set an exact box with :w=/:h=(PDF points), or pad the tag with spaces inside the braces — a padded tag's printed width becomes the field's width, so a tag stretched across a blank yields a field that fills the blank.

TagBecomes
{{sig}}Signature, signer 1, required
{{sig:s2}}Signature for signer 2
{{date:s1}}Date for signer 1
{{initials}}Initials, signer 1
{{text:s1:opt}}Optional text field
{{ text:s1 }}Text field as wide as the padded tag
{{name}} {{email}} {{check}}Name / email / checkbox

Prefill fields & named signers

Not every field is for a signer. Often you, the sender, need to fill in a few things first, like a contract date, an amount, or your own signature, before the document goes out. Those are prefill fields. Assign a field to recipientRef: 0 and it belongs to you, the preparer, instead of a signer. Signer slots stay 1..N, so a prefill field never adds a recipient and never blocks completion.

A prefill field can carry an optional default in prefillValue (text, an ISO date, "1" for a checked box, or a signature image URL). When you send, pass prefillData on POST /envelopes, a map of placement id to value, to fill them for that send. Leave it out and the defaults are used as-is. Either way the values are stamped into the document before any signer sees it.

# A prefill field is recipientRef 0. Place one with a default value:
PUT /v1/documents/{id}/fields
{"placements":[
  {"recipientRef":0,"fieldType":"TEXT","pageIndex":0,
   "x":0.12,"y":0.18,"w":0.25,"h":0.04,"required":true,
   "prefillValue":"Acme Corporation"},
  {"recipientRef":1,"fieldType":"SIGNATURE","pageIndex":0,
   "x":0.12,"y":0.80,"w":0.25,"h":0.06,"required":true}
]}

# Override the prefill value for this one send (else the default is used):
POST /v1/envelopes
{"documentId":"…","recipients":[{"email":"[email protected]","name":"Dana"}],
 "prefillData":{"<placement-id>":"Globex Inc"}}

Signer names are set when you prepare the document (in the editor) and are stored on it, so they carry across every send and pre-fill the recipient form. The name you pass per recipient on POST /envelopes still wins for that send and is what the audit trail records.

Endpoints

The complete reference lives in the OpenAPI spec. At a glance:

MethodPathSummary
GET/v1/mePlan, features, and limits for your key
POST/v1/documentsUpload a PDF (multipart, URL, or base64)
GET/v1/documentsList your documents
GET/v1/documents/{id}Fetch one document (status, templateName, lastDetection)
PATCH/v1/documents/{id}Set / rename the template name
DELETE/v1/documents/{id}Delete a document
GET/v1/documents/{id}/fieldsRead placed fields + page geometry
PUT/v1/documents/{id}/fieldsReplace the document's fields
POST/v1/envelopesSend a prepared document for signature
GET/v1/envelopesList envelopes
GET/v1/envelopes/{id}Track recipients + status
POST/v1/envelopes/{id}/voidCancel an active envelope
GET/v1/envelopes/{id}/pdfDownload the sealed signed PDF
GET/v1/envelopes/{id}/certificateDownload the certificate of completion
GET/POST/v1/contactsList or create contacts
PATCH/DELETE/v1/contacts/{id}Update or delete a contact

Rate limits, idempotency, and headers

Reads are capped at 1000 requests per minute and writes at 300, both per key owner. Every response carries X-Request-ID and X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset so you can back off before hitting a 429. Send an Idempotency-Key on POST /v1/documents and POST /v1/envelopes to make retries safe; a replayed response comes back with Idempotent-Replayed: true.

Webhooks

Rather than poll, subscribe a URL and Sign Sail will POST lifecycle events (envelope.sent, recipient.signed, envelope.completed, and the rest). Each delivery is signed with an X-SignSail-Signature: t=<unix_ms>,v1=<hex> header; v1 is the HMAC-SHA256 of "{t}.{body}" keyed by your webhook secret. The event shapes are in the OpenAPI webhooks section.