API reference

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.

Getting started

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.

SDKs

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.

Authentication

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:readList documents, read their status, fields, recipients, and audit trail, and download files.
documents:writeCreate documents from a PDF or a template, edit drafts, and delete documents.
documents:sendSend drafts, remind signers, void documents, and create signing sessions.
templates:readList templates and see their roles and fields.
events:readList the workspace's event history.
webhooks:manageCreate, change, and remove webhook endpoints.
agent:sendAn 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.

Errors

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_…" }
UNAUTHORIZED401No key, a revoked or expired key, or a malformed one.
INSUFFICIENT_SCOPE403The key lacks the scope this endpoint needs.
FORBIDDEN403The member the key acts for may not do this.
NOT_FOUND404No such resource in this workspace.
GONE410The document was deleted; it can be restored from the dashboard for 30 days.
INVALID_STATE409The resource is not in a state where this is possible, e.g. sending a sent document.
IDEMPOTENCY_IN_PROGRESS409The first request with this Idempotency-Key is still running.
VALIDATION422A field is missing or wrong; `param` names it.
IDEMPOTENCY_MISMATCH422This Idempotency-Key was used with a different request.
UPLOAD_NOT_FOUND422No file at that upload_id, or it expired.
NOT_A_PDF422The file is not a PDF.
FILE_TOO_LARGE422Over 25 MB.
PAYLOAD_TOO_LARGE413A JSON body over 1 MB or a multipart file over 4 MB.
UNSUPPORTED_MEDIA_TYPE415Send JSON with Content-Type application/json.
INVALID_JSON400The body is not valid JSON.
INVALID_PARAMETER400A query parameter is out of range; `param` names it.
INVALID_CURSOR400A cursor this API did not issue.
UNSUPPORTED_VERSION400Only 2026-09-09 is spoken.
TEST_MODE_ONLY403Test-mode helpers need a test key.
LIMIT_REACHED402A plan limit: live API access, the monthly document allowance, or a count cap.
RATE_LIMITED429Too many requests a minute for this key; the limit depends on the plan, and Retry-After says how long to wait.
INTERNAL500Our fault. Retry with the same Idempotency-Key; quote request_id to support.

Idempotency

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.

Rate limits

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.

Pagination

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.

Test mode

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.

Webhooks

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.sentA document was sent to its first signers.
document.viewedA signer opened the document.
recipient.signedA signer finished their part.
recipient.handed_offA signer passed the document to someone else.
document.declinedA signer declined, which stops the document.
document.completedEveryone signed; the sealed PDF is ready.
document.voidedThe sender voided the document.
document.expiredThe document expired unsigned.
document.deadline_changedThe sender moved the date the document closes.
document.reopenedThe sender reopened an expired document with a new date.

Embedded signing

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"
});

Agents

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.

Endpoints

Base URL https://sharesign.co/api. Ids are UUIDs. Times are ISO 8601 in UTC.

Who am I

GET /v1/me

The workspace, key, mode, plan limits, and this period's usage. Scope: documents:read.

Returns A `me` object.

List documents

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.

statusdraft, sent, completed, declined, voided, or expired.
limit1 to 100; default 25.
cursorThe next_cursor from the previous page.

Returns A list of `document` objects with `has_more` and `next_cursor`.

Create a document

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.

BodyType
title *string
upload_idstring
messagestring | null
redirect_urlstring | null
allow_handoffboolean
expires_in_daysinteger | null
reminder_daysarray of integer | null
recipientsarray of { name, email, role, order } Default [].
name *string
email *stringOne person per email on a document.
rolesigner | cccc receives copies and does not sign. Default signer.
orderintegerSigning order; equal numbers sign at the same time. Default 1.
fieldsarray of { recipient_email, type, page, x, y, width, height, required, label } Default [].
recipient_email *stringThe email of the recipient who fills it.
type *signature | initials | date | text | checkbox
page *integer1-based.
x, y, width, height *numberIn 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.
requiredbooleanDefault true.
labelstringShown to the signer; also how prefill finds template fields.
sendboolean Default false.
unitpt | 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 a document

GET /v1/documents/{id}

A deleted document answers 410 for 30 days, then 404. Scope: documents:read.

Returns The `document`.

Edit a draft

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.

BodyType
titlestring
messagestring | null
redirect_urlstring | null
allow_handoffboolean
expires_in_daysinteger | null
reminder_daysarray of integer | null
recipientsarray of { name, email, role, order }
name *string
email *stringOne person per email on a document.
rolesigner | cccc receives copies and does not sign. Default signer.
orderintegerSigning order; equal numbers sign at the same time. Default 1.
fieldsarray of { recipient_email, type, page, x, y, width, height, required, label }
recipient_email *stringThe email of the recipient who fills it.
type *signature | initials | date | text | checkbox
page *integer1-based.
x, y, width, height *numberIn 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.
requiredbooleanDefault true.
labelstringShown to the signer; also how prefill finds template fields.
unitpt | fraction Default "pt".

Returns The `document`.

Delete a 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 }`.

Send for signature

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`.

Remind signers

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] }`.

Change the deadline

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.

BodyType
expires_at *string

Returns The `document`, with its new `expires_at`; `sent` again if it had expired.

Void a document

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.

BodyType
reason *string

Returns The `document`, now `voided`.

Download a PDF

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.

filesigned (default) or original.
redirecttrue to be redirected to the file instead of receiving JSON.

Returns `{ url, expires_in, sha256 }`.

Where the document asks to be signed

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`.

Audit trail

GET /v1/documents/{id}/audit

Every event on the document, hash-chained, oldest first. Scope: documents:read.

Returns A list of `audit_event` objects.

Signing session

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.

BodyType
allowed_originstringThe origin that will frame the signing page, e.g. https://app.example.com. Omit for a plain link.
expires_inintegerSeconds the framing permission lasts. Default 86400.

Returns `{ url, embeddable, allowed_origin, expires_at, snippet }`.

Sign as a recipient (test mode)

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.

BodyType
valuesobject of stringValues 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`.

Decline as a recipient (test mode)

POST /v1/documents/{id}/recipients/{rid}/test_decline

Only with a test key on a test document. Scope: documents:send.

BodyType
reasonstring Default "Declined in a test".

Returns The `document`.

Start an upload

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.

BodyType
size_bytesintegerThe 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 }`.

List templates

GET /v1/templates

Active templates with their roles and fields. Templates are made in the dashboard. Scope: templates:read.

qFilter by name.
include_archivedtrue to include archived templates.

Returns A list of `template` objects.

Get a template

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`.

Create from a 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.

BodyType
rolesobject of object Default {}.
prefillobject of string Default {}.
titlestring
messagestring | null
redirect_urlstring | null
expires_in_daysinteger | null
reminder_daysarray of integer | null
sendboolean Default false.

Returns The `document`.

List events

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.

typedocument.sent, document.viewed, recipient.signed, recipient.handed_off, document.declined, document.completed, document.voided, document.expired, document.deadline_changed, document.reopened
document_idOnly this document's events.
after_sequenceEvents with a sequence above this, ascending.
before_sequenceEvents with a sequence below this, descending.
limit1 to 100; default 50.

Returns A list of `event` objects with `next_after_sequence` or `next_before_sequence`.

Get an event

GET /v1/events/{id}

One event with the document as it was at the time. Scope: events:read.

Returns The `event`.

List webhook endpoints

GET /v1/webhooks

Secrets are never included; see rotate_secret. Scope: webhooks:manage.

Returns A list of `webhook_endpoint` objects.

Add a webhook endpoint

POST /v1/webhooks

The signing secret is in this response and nowhere else afterwards. Up to 10 endpoints per workspace. Scope: webhooks:manage.

BodyType
url *stringA public https:// address.
descriptionstring | null
eventsarray of stringEvent types to receive, or ["*"] for all. Default ["*"].
modelive | testDefaults to the mode of the key making the request.

Returns The `webhook_endpoint`, with `secret`.

Get a webhook endpoint

GET /v1/webhooks/{id}

Status, failure count, last success and failure. Scope: webhooks:manage.

Returns The `webhook_endpoint`.

Change a 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.

BodyType
urlstringA public https:// address.
descriptionstring | null
eventsarray of stringEvent types to receive, or ["*"] for all. Default ["*"].
modelive | testDefaults to the mode of the key making the request.
statusactive | disabledTurning an endpoint on again also clears its failure count.

Returns The `webhook_endpoint`.

Remove a webhook endpoint

DELETE /v1/webhooks/{id}

Pending deliveries are dropped with it. Scope: webhooks:manage.

Returns `{ id, deleted: true }`.

Send a ping

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`.

Rotate the signing secret

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`.

List deliveries

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.

Replay an event

POST /v1/webhooks/{id}/replay

Sends an event from the history to this endpoint again, as a fresh delivery. Scope: webhooks:manage.

BodyType
event_id *string

Returns The `webhook_delivery`.