↗ · 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.mdAny 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.pdfAuto-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.
| Tag | Becomes |
|---|---|
| {{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:
| Method | Path | Summary |
|---|---|---|
| GET | /v1/me | Plan, features, and limits for your key |
| POST | /v1/documents | Upload a PDF (multipart, URL, or base64) |
| GET | /v1/documents | List 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}/fields | Read placed fields + page geometry |
| PUT | /v1/documents/{id}/fields | Replace the document's fields |
| POST | /v1/envelopes | Send a prepared document for signature |
| GET | /v1/envelopes | List envelopes |
| GET | /v1/envelopes/{id} | Track recipients + status |
| POST | /v1/envelopes/{id}/void | Cancel an active envelope |
| GET | /v1/envelopes/{id}/pdf | Download the sealed signed PDF |
| GET | /v1/envelopes/{id}/certificate | Download the certificate of completion |
| GET/POST | /v1/contacts | List 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.