Docs

Docs / Start / Quickstart

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