Quickstart
Four calls: create a template, publish it, prepare a document, send it.
Watch it done: Connect your software: the API, the SDK and the docs (2:38)
1. Create a template
Placeholders are {{like_this}}, and every one of them has to be
declared. Publishing checks that, so a contract cannot ship with an unfilled
placeholder in it.
curl https://api.signsealer.com/v1/templates \
-H "Authorization: Bearer $SIGNSEALER_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"code": "rental-agreement",
"name": "Short-term rental agreement",
"kind": "contract",
"variables": ["guest_name", "property", "arrival"],
"body": "This agreement is between Harbour Rentals and {{guest_name}} for {{property}}, arriving {{arrival}}."
}'
2. Publish it
The create answers {"template_id": "…"}. Publishing that
version freezes its text. From then on it is immutable and
POST /v1/templates with the same code is refused — revise it
instead (POST /v1/templates/{id}/revise), which makes version 2
and leaves version 1 exactly as the people who signed it saw it.
Templates and versions has the whole of it.
curl -X POST https://api.signsealer.com/v1/templates/$TEMPLATE/publish \
-H "Authorization: Bearer $SIGNSEALER_KEY" \
-H "Idempotency-Key: $(uuidgen)"
3. Prepare a document
The rendered text is stored on the document, not a reference to the template. That is what retention means: the template can change, and the whole point of versioning it is that it might.
curl https://api.signsealer.com/v1/documents \
-H "Authorization: Bearer $SIGNSEALER_KEY" \
-H "Content-Type: application/json" \
-d '{
"template_code": "rental-agreement",
"values": { "guest_name": "Dana Reyes", "property": "River Bend Cabin", "arrival": "3 October" }
}'
4. Send it
You get one link per signer, once. The tokens are never stored — only their
hashes — so a database dump is not a set of signing links, and a lost link is
replaced rather than looked up: POST /v1/documents/{id}/signers/{signer}/remind
sends a fresh one.
Each signer needs an email, a phone, or both. Given
both, the same link goes out twice — by mail and by text — because a
person who does not check one inbox checks the other. A text goes only where the
business holds consent for signing.link; where it does not, the
email still goes and the reason is on the evidence trail.
A new business sends to a few people an hour at first, and to more as it is
trusted (Limits and paging); past that the send is
429 and nobody on it is sent anything. Every message is read
before it leaves, so the answer means the links were made, not that they
arrived.
curl https://api.signsealer.com/v1/documents/$DOC/send \
-H "Authorization: Bearer $SIGNSEALER_KEY" \
-H "Content-Type: application/json" \
-d '{"signers":[{"email":"dana@example.signsealer.com","phone":"+16155550142","full_name":"Dana Reyes"}]}'
The same in Node
@signsealer/sdk has no dependencies and runs anywhere
fetch does. It adds the idempotency key itself and retries only
the failures worth retrying.
import { SignSealer } from "@signsealer/sdk";
const signsealer = new SignSealer({ apiKey: process.env.SIGNSEALER_KEY! });
const { template_id } = await signsealer.createTemplate({
code: "rental-agreement", name: "Short-term rental agreement", kind: "contract",
variables: ["guest_name", "property", "arrival"],
body: "This agreement is between Harbour Rentals and {{guest_name}} for {{property}}, arriving {{arrival}}.",
});
await signsealer.publishTemplate(template_id);
const { document_id } = await signsealer.prepareDocument({
template_code: "rental-agreement",
values: { guest_name: "Dana Reyes", property: "River Bend Cabin", arrival: "3 October" },
external_ref: "RES-88213", // yours; every webhook about it carries it back
});
const { links } = await signsealer.send(document_id, [
{ email: "dana@example.signsealer.com", phone: "+1 615 555 0142", full_name: "Dana Reyes" },
]);
The same in Python
No client library yet; the API is small enough that requests
is the client. Send your own Idempotency-Key on the writes, and
retry only 429 and 500.
import os, uuid, requests
API = "https://api.signsealer.com"
headers = {"Authorization": f"Bearer {os.environ['SIGNSEALER_KEY']}"}
def post(path, body):
r = requests.post(API + path, json=body, timeout=15,
headers={**headers, "Idempotency-Key": str(uuid.uuid4())})
r.raise_for_status()
return r.json()
doc = post("/v1/documents", {
"template_code": "rental-agreement",
"values": {"guest_name": "Dana Reyes", "property": "River Bend Cabin", "arrival": "3 October"},
})
links = post(f"/v1/documents/{doc['document_id']}/send", {
"signers": [{"email": "dana@example.signsealer.com", "phone": "+16155550142", "full_name": "Dana Reyes"}],
})["links"]
Then what? Add a
webhook and wait for
signing.completed. Polling works and is the wrong shape: a
completion is an event, and the event carries everything you need to act
without a second call.
Ready to build? An API key takes a minute in the portal, and the free plan covers the first 10 agreements a month.
Get an API key