Signing a document is a short story with four parts. Your software decides a document is needed. Someone signs it. You get told. You keep a copy you can prove has not changed. This post does all four with the Docustay API, from a test key, so nothing real is sent and nothing is billed.
Docustay (https://docustay.app) is an e-signature server under the AGPL-3.0, with SDKs under MIT. You can use the hosted service or run your own. I make it, so read what follows with that in mind. I will say plainly what is not finished.
Want to look before you read? The demo needs no account and no card: https://docustay.app/demo. You get a small property-management workspace where you can send a document, sign it yourself and ask Scribe, the built-in AI assistant, to draft one. Nothing is emailed, and the demo is deleted after 24 hours.
Two things matter if you are a developer or you run your own servers. Pro is one flat price for the whole workspace, with no per-document or per-call fees. And the code is open, so the same product runs on your own server.
What you need
- A Docustay workspace on the Free plan. It includes the API, webhooks, embedding and test mode, inside 15 documents and 5 new templates a month. Test documents never count.
- A test key. In the app open Developers, then API keys, and make a key with the scopes
documents:readanddocuments:write. A key that startsdsk_test_only makes practice runs: the document is emailed to you alone, and every page carries a TEST watermark. - A template, with its id. Make one in the app (Templates, then Create) and copy the id from the address bar.
- Node 18 or newer.
Install the SDK with npm install @docustay2026/sdk. Its code is in the sdk/js folder of https://github.com/Docustay/docustay. There is a Python client too (pip install docustay), a command line tool (npx docustay), and an MCP server (@docustay2026/mcp) that lets an AI assistant send documents for signature and follow them. The SDKs are listed at https://docustay.app/developers/sdks.
1. Send a document
import { Docustay } from "@docustay2026/sdk";
const docustay = new Docustay(process.env.DOCUSTAY_API_KEY, process.env.DOCUSTAY_URL ? { baseUrl: process.env.DOCUSTAY_URL } : {});
const doc = await docustay.sendDocument({
templateId: process.env.DOCUSTAY_TEMPLATE_ID,
parties: [{ seat: "signer_1", name: "Pat Example", email: "pat@example.com" }],
});
console.log(doc.id, doc.state);
A seat is the template's name for a signer: the first role in the document is signer_1, the next signer_2. The client makes an Idempotency-Key for the call and reuses it on every retry, so a network retry never sends the document twice. Over the rate limit (600 requests a minute per key) it waits for Retry-After.
Run it with a test key and look in your own inbox, not Pat's. The email is addressed to you and its subject has [TEST] in it. doc.state moves from sent through partially_signed to executed.
2. Show the signing form in your page
An emailed link is fine for a stranger. If the signer is already in your app, you can keep them there. Three steps.
First, list your website under Developers, then Embedding. http://localhost:3000 is allowed while you build. Second, send with the seat marked as embedded, so that nobody is emailed for it:
import { Docustay } from "@docustay2026/sdk";
const docustay = () => new Docustay(process.env.DOCUSTAY_API_KEY, process.env.DOCUSTAY_URL ? { baseUrl: process.env.DOCUSTAY_URL } : {});
// POST { name, email } → sends the template to that person, to be signed inside your page.
export async function POST(req) {
const { name, email } = await req.json();
try {
const doc = await docustay().sendDocument({ templateId: process.env.DOCUSTAY_TEMPLATE_ID, embedded: ["signer_1"], parties: [{ seat: "signer_1", name, email }] });
return Response.json({ id: doc.id });
} catch (e) { return Response.json({ error: e.message }, { status: e.status ?? 500 }); }
}
Third, on your server, ask for a session. It is a link that works for one hour, so make a new one each time the page loads. The key never reaches the browser:
import { Docustay } from "@docustay2026/sdk";
import Form from "./form";
// A server component: the one-hour link is made on the server, so the API key never reaches the browser.
export default async function Page({ searchParams }) {
const { id } = await searchParams;
const docustay = new Docustay(process.env.DOCUSTAY_API_KEY, process.env.DOCUSTAY_URL ? { baseUrl: process.env.DOCUSTAY_URL } : {});
const { url } = await docustay.embedSession(id, { seat: "signer_1", origin: process.env.PUBLIC_ORIGIN ?? "http://localhost:3000" });
return <Form url={url} />;
}
The browser gets only that url. Give it to the element:
<script type="module" src="https://docustay.app/embed/index.js"></script>
<docustay-sign url="https://…/sign/pdf?t=…&embed=1&parent=https%3A%2F%2Fapp.example.com" height="720"></docustay-sign>
The element fires ready, waiting, completed, declined, error and resize. Treat completed as a prompt, not as proof: anyone can fire an event in a browser. Confirm on your server by reading the document and checking that state is executed. The form can be framed only by the websites you listed, and an ordinary emailed link can never be framed.
3. Check the webhook
Polling works, but you were going to be told anyway. Add an endpoint under Developers, then Webhooks, choose documents.document.executed, and keep the secret that starts whsec_. It is shown once.
import { verifyWebhook } from "@docustay2026/sdk";
// The signature is over the exact bytes Docustay sent, so read the body as text and do not parse it first.
export async function POST(req) {
try {
const event = await verifyWebhook(process.env.DOCUSTAY_WEBHOOK_SECRET, req.headers, await req.text());
console.log("webhook", event.topic, event.subject_id);
return new Response(null, { status: 204 });
} catch (e) { return new Response("bad signature", { status: 400 }); }
}
Three things matter here. Read the raw body before any JSON parser touches it, because the signature is over those exact bytes. Expect repeats and out-of-order deliveries: the same event carries the same webhook-id, so skip ids you have seen. And do not treat the event as the last word; fetch the document. A failed delivery is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, and every attempt is in the delivery log, where you can replay it while you fix your receiver.
Docustay only posts to a public https:// address, so on a laptop you need a tunnel, or the CLI's docustay listen --forward-to, which fetches the real signed deliveries to your machine without opening anything to the internet.
4. Get the sealed PDF and check it
When everyone has signed, the sealed copy is made a few seconds later; the document says signedCopyAvailable: true once it exists. Until then the call answers 404, "the sealed copy is being made", so wait for the executed event or ask again.
curl https://docustay.app/api/v1/documents/DOCUMENT_ID/signed.pdf \
-H "Authorization: Bearer $DOCUSTAY_KEY" -o signed.pdf
Open signed.pdf. The last page is a certificate: each person's name, email and role, the time and address of each action, the wording they agreed to, and a SHA-256 fingerprint of the document. The whole file carries a detached PKCS#7 digital signature, stamped by a time-stamping authority (RFC 3161). Change a byte afterwards and the seal no longer matches.
To check it, drop signed.pdf on the verify page at https://docustay.app/verify. It reads the seal and the fingerprint from the file and tells you whether the file is intact. The check runs in your browser and the file is not uploaded. A self-hosted install has the same page at /verify. There is no scripted verifier in the SDKs today; the verify page is the supported check.
Two honest caveats. By default the seal comes from a certificate Docustay makes for the deployment, so a PDF reader may show the seal as intact and the certificate as not trusted; self-hosters set their own. And if the time service cannot be reached, the document is still sealed and the certificate says it was not timestamped.
Run it yourself
If you would rather not send documents through someone else's server, run the whole product on your own. You need Git, Docker and Docker Compose.
git clone https://github.com/Docustay/docustay.git
cd docustay/docustay
./init.sh
docker compose up -d
Then open http://localhost:8080/setup, enter the setup token that init.sh printed, and create the owner account. Prebuilt images for x86 and ARM are published at ghcr.io/docustay, so you do not have to build them on your server.
What you are running is six containers: the database, a file store, a PDF converter, the app, a worker and the web server. One measurement on one real install put it at about 1 GiB of memory when idle. The database is PostgreSQL 16 with row-level security on every table that holds workspace data. Your documents stay on your machine. Email goes through a Resend key or your own SMTP server, and payments use your own Stripe keys.
Three things self-hosters ask about. There is one anonymous daily usage ping, which carries nothing about your documents or people; switch it off with DOCUSTAY_TELEMETRY=off. Sign-in can use your own OpenID Connect provider. And a backup is four files, while an upgrade is three commands; the steps are on the self-host page.
Licences, as listed on the self-host page today: Community is free under the AGPL-3.0, with no monthly limits. Self-host Pro is $390 a year per install, for your own business. A commercial licence, from about $3,000 a year, is for embedding Docustay in a product, white-labelling it, reselling it or keeping a private fork. The full guide is at https://docustay.app/self-host.
What this is, and is not
A Docustay signature is a simple electronic signature with a strong evidence record and a digital seal. It is not a qualified electronic signature and it checks no identity beyond an email address or an access code you share. No outside audit or certification has been done, and none is claimed. It is not for protected health information. This is not legal advice.
Uploads are PDF only, and nothing is converted. Every language except English is a machine-written draft marked Beta.
Where to go next
- Try the demo, no account needed: https://docustay.app/demo
- The Docustay website: https://docustay.app
- The developer pages: https://docustay.app/developers
- The quickstart and API reference: https://docustay.app/docs/quickstart
- The same flow in a runnable Next.js app:
docustay/examples/nextjsin https://github.com/Docustay/docustay - Test mode, in full: https://docustay.app/developers/test-mode
- The self-host guide, with requirements, backups and upgrades: https://docustay.app/self-host
Free is 15 documents a month. Pro is one flat price for the whole workspace, with unlimited users and no per-call fees; current prices are at https://docustay.app/pricing. I read every reply.
Top comments (0)