Who am I
GET /v1/me
The workspace, key, mode, plan limits, and this period's usage. Scope: documents:read.
Returns A `me` object.
Everything the dashboard does, your code can do too. One base URL, bearer keys, JSON in and out, and webhooks when something changes. Version 2026-09-09. OpenAPI document.
Create a key in Settings, Developers. A test key works on every plan and signs for real without emailing anyone; a live key comes with the Pro and Business plans. Pro includes a monthly allowance of documents sent through the API; Business has no monthly API limit. Then send a document from a template in one call:
curl -X POST https://sharesign.co/api/v1/templates/{template_id}/documents \
-H "Authorization: Bearer $SHARESIGN_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1001" \
-d '{ "roles": { "Tenant": { "name": "Nora Whelan", "email": "nora@example.com" } }, "send": true }'The response is the document, with a url to it in the dashboard. Without a template, POST a PDF as multipart with recipients and fields placed in points from the top-left of the page as it is displayed. The Developers page shows both, filled in with your own template.
Official clients for TypeScript and Python are generated from this API's OpenAPI document, so their types are the API's types. They add what a client should: automatic idempotency keys, retries that honour Retry-After, iterators over lists and events, a file helper that picks inline or upload, typed errors with the request id, and webhook verification.
npm install @sharesign/sdk # Node 18+, Deno, Bun, edge runtimes pip install sharesign # Python 3.10+, sync and async
import ShareSign from "@sharesign/sdk";
const sharesign = new ShareSign({ apiKey: process.env.SHARESIGN_API_KEY! });
const doc = await sharesign.templates.createDocument(templateId, {
roles: { Tenant: { name: "Nora Whelan", email: "nora@example.com" } },
send: true,
});
for await (const d of sharesign.documents.iterate({ status: "completed" })) console.log(d.title);from sharesign import ShareSign
sharesign = ShareSign(api_key=os.environ["SHARESIGN_API_KEY"])
doc = sharesign.templates.create_document(template_id, {"roles": {"Tenant": {"name": "Nora Whelan", "email": "nora@example.com"}}, "send": True})
for d in sharesign.documents.iterate(status="completed"):
print(d["title"])Source and issues: github.com/prasants/sharesign/sdk.
Send the key as a bearer token on every request. Keys belong to the workspace and act as the member who made them, or as the workspace owner once that member has left, so an integration outlives the person who set it up. Each key carries scopes:
| documents:read | List documents, read their status, fields, recipients, and audit trail, and download files. |
|---|---|
| documents:write | Create documents from a PDF or a template, edit drafts, and delete documents. |
| documents:send | Send drafts, remind signers, void documents, and create signing sessions. |
| templates:read | List templates and see their roles and fields. |
| events:read | List the workspace's event history. |
| webhooks:manage | Create, change, and remove webhook endpoints. |
| agent:send | An agent using this key may send, remind, void and delete on its own, without a person pressing Send. Needs the send scope as well. Off by default; grant it to one key for one automation. |
Keys can expire on a date you set, and are revoked instantly. The Developers page shows when each was last used. The optional ShareSign-Version header pins the API shape; the only value today is 2026-09-09, echoed back on every response.
Every error is JSON with a stable code, a sentence for a person, the parameter at fault when there is one, and a request id to quote to support.
{ "error": { "code": "VALIDATION", "message": "fields[0].page: the file has 2 pages.", "param": "fields[0].page" }, "request_id": "req_…" }| UNAUTHORIZED | 401 | No key, a revoked or expired key, or a malformed one. |
|---|---|---|
| INSUFFICIENT_SCOPE | 403 | The key lacks the scope this endpoint needs. |
| FORBIDDEN | 403 | The member the key acts for may not do this. |
| NOT_FOUND | 404 | No such resource in this workspace. |
| GONE | 410 | The document was deleted; it can be restored from the dashboard for 30 days. |
| INVALID_STATE | 409 | The resource is not in a state where this is possible, e.g. sending a sent document. |
| IDEMPOTENCY_IN_PROGRESS | 409 | The first request with this Idempotency-Key is still running. |
| VALIDATION | 422 | A field is missing or wrong; `param` names it. |
| IDEMPOTENCY_MISMATCH | 422 | This Idempotency-Key was used with a different request. |
| UPLOAD_NOT_FOUND | 422 | No file at that upload_id, or it expired. |
| NOT_A_PDF | 422 | The file is not a PDF. |
| FILE_TOO_LARGE | 422 | Over 25 MB. |
| PAYLOAD_TOO_LARGE | 413 | A JSON body over 1 MB or a multipart file over 4 MB. |
| UNSUPPORTED_MEDIA_TYPE | 415 | Send JSON with Content-Type application/json. |
| INVALID_JSON | 400 | The body is not valid JSON. |
| INVALID_PARAMETER | 400 | A query parameter is out of range; `param` names it. |
| INVALID_CURSOR | 400 | A cursor this API did not issue. |
| UNSUPPORTED_VERSION | 400 | Only 2026-09-09 is spoken. |
| TEST_MODE_ONLY | 403 | Test-mode helpers need a test key. |
| LIMIT_REACHED | 402 | A plan limit: live API access, the monthly document allowance, or a count cap. |
| RATE_LIMITED | 429 | Too many requests a minute for this key; the limit depends on the plan, and Retry-After says how long to wait. |
| INTERNAL | 500 | Our fault. Retry with the same Idempotency-Key; quote request_id to support. |
Add an Idempotency-Key header (any string up to 255 characters, unique per operation) to a POST, PATCH, or DELETE. If the connection drops and you retry with the same key and body, you get the original response back, marked Idempotent-Replayed: true, and nothing happens twice. A reused key with a different body is refused. Keys are remembered for 24 hours. A 5xx frees the key so the retry runs.
Each key may make a set number of requests a minute, which depends on the plan. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; a 429 carries Retry-After in seconds. Documents sent by API count against the plan's monthly allowance exactly as the dashboard's do.
Lists return data, has_more, and next_cursor. Pass the cursor back to get the next page. Cursors are stable: a document created while you page never shifts the rest. Events page by sequence instead, so a consumer can ask for everything after the last one it saw.
A test key (ss_test_…) creates test documents: they render, sign, seal, and audit exactly like live ones, but no email is sent, nothing counts against the plan, and they appear in the dashboard marked Test. Two helpers exist only in test mode: sign or decline as any recipient, so a test suite can drive a document to completion and assert on the webhooks it receives. Test events go only to test-mode endpoints and say livemode: false.
Add an endpoint in Settings or by API and choose events. Each delivery is a POST with a JSON event and a signature header:
POST https://example.com/sharesign
Content-Type: application/json
ShareSign-Signature: t=1757404800,v1=5f8c…
ShareSign-Event-Type: document.completed
ShareSign-Delivery-Id: 6b1e…
{ "id": "…", "object": "event", "type": "document.completed", "sequence": 1042, "livemode": true,
"created_at": "2026-09-09T08:00:00.000Z", "api_version": "2026-09-09",
"data": { "document": { … the document as it was … }, "recipient": { … when one is involved … } } }Verify the signature before trusting the body. The SDKs do it in one call: constructEvent(rawBody, header, secret) in TypeScript, construct_event(raw_body, header, secret) in Python. By hand: the signed string is the timestamp, a dot, and the raw body; the digest is HMAC-SHA256 with your endpoint's secret, hex encoded. Reject anything older than five minutes to stop replays.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody: string, header: string, secret: string) {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=") as [string, string]));
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const sigs = header.split(",").filter((kv) => kv.startsWith("v1=")).map((kv) => kv.slice(3));
return fresh && sigs.some((s) => s.length === expected.length && timingSafeEqual(Buffer.from(s, "hex"), Buffer.from(expected, "hex")));
}Answer with any 2xx within ten seconds; do the work afterwards. Anything else is retried 9 more times over about three days. Deliveries are not guaranteed to arrive in order: use sequence, or fetch the document, before acting on state. An endpoint that fails 30 times in a row is paused and the workspace's owners are emailed; turning it back on, replaying events, and sending a test ping are all one click in Settings. Rotating the secret keeps both secrets valid for 24 hours. Event types:
| document.sent | A document was sent to its first signers. |
|---|---|
| document.viewed | A signer opened the document. |
| recipient.signed | A signer finished their part. |
| recipient.handed_off | A signer passed the document to someone else. |
| document.declined | A signer declined, which stops the document. |
| document.completed | Everyone signed; the sealed PDF is ready. |
| document.voided | The sender voided the document. |
| document.expired | The document expired unsigned. |
| document.deadline_changed | The sender moved the date the document closes. |
| document.reopened | The sender reopened an expired document with a new date. |
To let someone sign inside your own product, ask for a signing session naming the origin that will frame it. The URL you get back may be placed in an iframe on that origin, and nowhere else, until it expires. The page tells the parent window what happened:
const res = await fetch(`https://sharesign.co/api/v1/documents/${docId}/recipients/${recipientId}/signing_session`, {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: JSON.stringify({ allowed_origin: "https://app.example.com" }),
});
const { url } = await res.json();
// <iframe src={url} style="width:100%;height:100vh;border:0"></iframe>
window.addEventListener("message", (e) => {
if (e.origin !== "https://sharesign.co" || e.data?.source !== "sharesign") return;
// e.data.event is "viewed", "signed", "declined", "handed_off", or "completed"
});ShareSign is a Model Context Protocol server at https://sharesign.co/mcp, so Claude, ChatGPT, or an agent you wrote yourself can work with your documents from wherever it already runs. There are two ways to connect, and which one you use decides what the agent is.
As you. In claude.ai, Claude Desktop or ChatGPT, add https://sharesign.co/mcp as a custom connector. You are sent to sign in to ShareSign and shown exactly what the agent is asking for; once you allow it, the agent acts as you, in the workspace you last used in the dashboard, with your role’s permissions. It can read, draft and prepare; it can never send, remind, void or delete. You press Send. To end its access, disconnect ShareSign in the app: a token it already holds is good for at most 24 hours and cannot be renewed after that.
With a key. Claude Code, Cursor, and anything you write yourself can carry an API key as the bearer token instead. The key’s scopes apply exactly as they do here: an agent sees only the tools its key allows, and nothing it cannot do through this API becomes possible through an agent.
{
"mcpServers": {
"sharesign": {
"url": "https://sharesign.co/mcp",
"headers": { "Authorization": "Bearer ss_live_..." }
}
}
}With documents:read and templates:read (or, as you, a role that can read) an agent can list templates and their roles, list documents by status, read a document’s recipients and fields, read its activity to say where it stalled, get a fifteen-minute download link, and ask where the document itself asks to be signed — the same detection the editor offers as ghost fields, returned as fields to place with the recipient each one matched, so an agent never invents a coordinate.
With documents:write it can also draft: create a document from a template by naming who plays each role, or from its own PDF through an upload URL; edit a draft’s recipients, fields and message; and prepare it for sending. Preparing runs the same checks Send does and leaves the draft in ShareSign marked prepared by an agent, with the recipients read back — a person presses Send. No tool sends, reminds, voids or deletes unless the key also carries agent:send beside documents:send. That scope is off by default, is granted to one key for one automation, and cannot be granted to a connection made as you; with it, the agent can send a document it prepared (naming the version it prepared, so nothing that changed in between goes out), remind signers, void, and delete. Without it, the person is the Send button.
Every write takes an idempotency_key so a retried call is answered with the first call’s result rather than done twice, and every edit names the version it read, so an agent never overwrites a person’s edit: if the document changed in between, the edit is refused and the agent reads again. What an agent does lands in the document’s audit trail beside what people do, naming the key or the app. A test key works throughout: documents sign for real and email nobody.
Two things an agent never receives, by design: a signing link, and the document’s own text. A signing link is a recipient’s identity; the text of a contract can carry instructions to a model. Status, recipients, fields and activity are what an agent needs.
Base URL https://sharesign.co/api. Ids are UUIDs. Times are ISO 8601 in UTC.
GET /v1/me
The workspace, key, mode, plan limits, and this period's usage. Scope: documents:read.
Returns A `me` object.
GET /v1/documents
Newest first. Deleted and internal documents are not listed. A live key sees live documents; a test key sees test documents. Scope: documents:read.
| status | draft, sent, completed, declined, voided, or expired. |
|---|---|
| limit | 1 to 100; default 25. |
| cursor | The next_cursor from the previous page. |
Returns A list of `document` objects with `has_more` and `next_cursor`.
POST /v1/documents
From a PDF. Send JSON with an `upload_id` (see Uploads), or `multipart/form-data` with a `file` part (up to 4 MB), a `title` part, and an optional `document` part holding the JSON below. Recipients, fields, and `send: true` can all be given at once. Scope: documents:write.
| Body | Type | |
|---|---|---|
| title * | string | |
| upload_id | string | |
| message | string | null | |
| redirect_url | string | null | |
| allow_handoff | boolean | |
| expires_in_days | integer | null | |
| reminder_days | array of integer | null | |
| recipients | array of { name, email, role, order } | Default []. |
| name * | string | |
| email * | string | One person per email on a document. |
| role | signer | cc | cc receives copies and does not sign. Default signer. |
| order | integer | Signing order; equal numbers sign at the same time. Default 1. |
| fields | array of { recipient_email, type, page, x, y, width, height, required, label } | Default []. |
| recipient_email * | string | The email of the recipient who fills it. |
| type * | signature | initials | date | text | checkbox | |
| page * | integer | 1-based. |
| x, y, width, height * | number | In PDF points from the top-left of the page as it is displayed, after any crop or rotation, or fractions of the page when unit is fraction. A field must fit on its page. |
| required | boolean | Default true. |
| label | string | Shown to the signer; also how prefill finds template fields. |
| send | boolean | Default false. |
| unit | pt | fraction | Default "pt". |
Or multipart/form-data: `file` (the PDF), `title`, and optionally `document` (a JSON string with the fields below).
Returns The `document`.
GET /v1/documents/{id}
A deleted document answers 410 for 30 days, then 404. Scope: documents:read.
Returns The `document`.
PATCH /v1/documents/{id}
Only drafts change. `recipients` and `fields`, when given, replace the whole set; recipients keep their ids when their email is unchanged. Scope: documents:write.
| Body | Type | |
|---|---|---|
| title | string | |
| message | string | null | |
| redirect_url | string | null | |
| allow_handoff | boolean | |
| expires_in_days | integer | null | |
| reminder_days | array of integer | null | |
| recipients | array of { name, email, role, order } | |
| name * | string | |
| email * | string | One person per email on a document. |
| role | signer | cc | cc receives copies and does not sign. Default signer. |
| order | integer | Signing order; equal numbers sign at the same time. Default 1. |
| fields | array of { recipient_email, type, page, x, y, width, height, required, label } | |
| recipient_email * | string | The email of the recipient who fills it. |
| type * | signature | initials | date | text | checkbox | |
| page * | integer | 1-based. |
| x, y, width, height * | number | In PDF points from the top-left of the page as it is displayed, after any crop or rotation, or fractions of the page when unit is fraction. A field must fit on its page. |
| required | boolean | Default true. |
| label | string | Shown to the signer; also how prefill finds template fields. |
| unit | pt | fraction | Default "pt". |
Returns The `document`.
DELETE /v1/documents/{id}
A sent document is voided first. It can be restored from the dashboard for 30 days, then its files are purged. Scope: documents:write.
Returns `{ id, deleted: true, restorable_until }`.
POST /v1/documents/{id}/send
Every signer needs at least one field. Signers in the first wave are emailed at once; the rest when their turn comes. Counts against the plan's monthly documents (never for test documents). Scope: documents:send.
Returns The `document`, now `sent`.
POST /v1/documents/{id}/remind
Emails a fresh signing link to everyone whose turn it is. Reminders also go out on the schedule the sender chose (`reminder_days`), and nobody is emailed twice within 20 hours. Scope: documents:send.
Returns `{ id, reminded: [recipient ids] }`.
POST /v1/documents/{id}/deadline
Moves when a sent document closes, or reopens one that expired; nobody who signed signs again. `expires_at` must carry its time zone, be at least an hour ahead, and fall within a year of when the document was sent. Reopening emails everyone still waiting, which is why this needs the send scope. Reminders and the last call follow the new date by themselves. Scope: documents:send.
| Body | Type | |
|---|---|---|
| expires_at * | string |
Returns The `document`, with its new `expires_at`; `sent` again if it had expired.
POST /v1/documents/{id}/void
Stops a draft or sent document. Signing links stop working; signers who open one see the sender withdrew it. Scope: documents:send.
| Body | Type | |
|---|---|---|
| reason * | string |
Returns The `document`, now `voided`.
GET /v1/documents/{id}/download
A URL valid for 15 minutes. The signed PDF exists once the document is completed; the original is always available. Scope: documents:read.
| file | signed (default) or original. |
|---|---|
| redirect | true to be redirected to the file instead of receiving JSON. |
Returns `{ url, expires_in, sha256 }`.
GET /v1/documents/{id}/suggestions
Reads the document's own text for signature, initials, date and name blanks — "Signature: ____", "Landlord Signature" — and returns each as a field to place, in points on a page of the whole document, with the recipient it matched when the document named a party. The same detection the editor offers as ghost fields. Precision over recall: a place it is unsure of is left out. `text_found` is false for a scanned PDF, which has nothing to read. Scope: documents:read.
Returns A list of `suggestion` objects with `text_found`.
GET /v1/documents/{id}/audit
Every event on the document, hash-chained, oldest first. Scope: documents:read.
Returns A list of `audit_event` objects.
POST /v1/documents/{id}/recipients/{rid}/signing_session
A URL where this recipient can sign, for use inside your own product. With `allowed_origin`, the page may be framed by that origin until `expires_at`, and posts `{ source: "sharesign", event, document_id, recipient_id }` to the parent window on `viewed`, `signed`, `declined`, `handed_off`, and `completed`. Scope: documents:send.
| Body | Type | |
|---|---|---|
| allowed_origin | string | The origin that will frame the signing page, e.g. https://app.example.com. Omit for a plain link. |
| expires_in | integer | Seconds the framing permission lasts. Default 86400. |
Returns `{ url, embeddable, allowed_origin, expires_at, snippet }`.
POST /v1/documents/{id}/recipients/{rid}/test_sign
Only with a test key on a test document: completes this recipient's fields, so you can drive a whole flow from a test suite. Scope: documents:send.
| Body | Type | |
|---|---|---|
| values | object of string | Values for this recipient's text, date, and checkbox fields, by field id or label. Signatures and initials are drawn for you. Default {}. |
Returns The `document`.
POST /v1/documents/{id}/recipients/{rid}/test_decline
Only with a test key on a test document. Scope: documents:send.
| Body | Type | |
|---|---|---|
| reason | string | Default "Declined in a test". |
Returns The `document`.
POST /v1/uploads
For PDFs over 4 MB (up to 25 MB): a URL to PUT the file to within 15 minutes, then pass the `id` as `upload_id` when creating a document. Send `size_bytes` and the URL accepts only a file of exactly that size. An upload not used within a day is refused and removed. Scope: documents:write.
| Body | Type | |
|---|---|---|
| size_bytes | integer | The file's exact size in bytes. When given, the upload URL accepts only a file of exactly this size. |
Returns `{ id, url, method: "PUT", headers, max_bytes, expires_in }`.
GET /v1/templates
Active templates with their roles and fields. Templates are made in the dashboard. Scope: templates:read.
| q | Filter by name. |
|---|---|
| include_archived | true to include archived templates. |
Returns A list of `template` objects.
GET /v1/templates/{id}
Roles (with whether each is pinned to a person) and fields (with whether each can be prefilled). Scope: templates:read.
Returns The `template`.
POST /v1/templates/{id}/documents
Roles are matched by name or id; prefill by field label or id. Merge tags in the template's title and message are resolved. The usual ten-line integration. Scope: documents:write.
| Body | Type | |
|---|---|---|
| roles | object of object | Default {}. |
| prefill | object of string | Default {}. |
| title | string | |
| message | string | null | |
| redirect_url | string | null | |
| expires_in_days | integer | null | |
| reminder_days | array of integer | null | |
| send | boolean | Default false. |
Returns The `document`.
GET /v1/events
What happened, newest first. Pass `after_sequence` to receive older-to-newer from a point: the shape a poller wants, and a way to catch up after a webhook outage. Events are kept for 90 days. Scope: events:read.
| type | document.sent, document.viewed, recipient.signed, recipient.handed_off, document.declined, document.completed, document.voided, document.expired, document.deadline_changed, document.reopened |
|---|---|
| document_id | Only this document's events. |
| after_sequence | Events with a sequence above this, ascending. |
| before_sequence | Events with a sequence below this, descending. |
| limit | 1 to 100; default 50. |
Returns A list of `event` objects with `next_after_sequence` or `next_before_sequence`.
GET /v1/events/{id}
One event with the document as it was at the time. Scope: events:read.
Returns The `event`.
GET /v1/webhooks
Secrets are never included; see rotate_secret. Scope: webhooks:manage.
Returns A list of `webhook_endpoint` objects.
POST /v1/webhooks
The signing secret is in this response and nowhere else afterwards. Up to 10 endpoints per workspace. Scope: webhooks:manage.
| Body | Type | |
|---|---|---|
| url * | string | A public https:// address. |
| description | string | null | |
| events | array of string | Event types to receive, or ["*"] for all. Default ["*"]. |
| mode | live | test | Defaults to the mode of the key making the request. |
Returns The `webhook_endpoint`, with `secret`.
GET /v1/webhooks/{id}
Status, failure count, last success and failure. Scope: webhooks:manage.
Returns The `webhook_endpoint`.
PATCH /v1/webhooks/{id}
Change the URL, events, or turn it on or off. A paused endpoint is turned on with `status: "active"`. Scope: webhooks:manage.
| Body | Type | |
|---|---|---|
| url | string | A public https:// address. |
| description | string | null | |
| events | array of string | Event types to receive, or ["*"] for all. Default ["*"]. |
| mode | live | test | Defaults to the mode of the key making the request. |
| status | active | disabled | Turning an endpoint on again also clears its failure count. |
Returns The `webhook_endpoint`.
DELETE /v1/webhooks/{id}
Pending deliveries are dropped with it. Scope: webhooks:manage.
Returns `{ id, deleted: true }`.
POST /v1/webhooks/{id}/test
Queues a `ping` event so you can watch your handler receive and verify a real delivery. Scope: webhooks:manage.
Returns The `webhook_delivery`.
POST /v1/webhooks/{id}/rotate_secret
A new secret at once. Deliveries carry two signatures for 24 hours, one for each secret, so you can switch without dropping an event. Scope: webhooks:manage.
Returns The `webhook_endpoint`, with the new `secret`.
GET /v1/webhooks/{id}/deliveries
The last 50 deliveries to this endpoint with status, attempts, response code, and error. Scope: webhooks:manage.
Returns A list of `webhook_delivery` objects.
POST /v1/webhooks/{id}/replay
Sends an event from the history to this endpoint again, as a fresh delivery. Scope: webhooks:manage.
| Body | Type | |
|---|---|---|
| event_id * | string |
Returns The `webhook_delivery`.