cd /news/ai-tools/send-a-document-for-signature-from-c… · home › topics › ai-tools › article
[ARTICLE · art-159777] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

Send a document for signature from code, embed the form, and check the seal

The developer behind Docustay, an AGPL-3.0 e-signature server with MIT-licensed SDKs, published a walkthrough showing how to send documents for signature programmatically, embed the signing form in an existing app, and verify the resulting seal. The guide covers the JavaScript SDK's idempotency-key handling and rate-limit backoff, test keys that watermark pages and route email to the sender only, and a one-hour embedded signing session generated server-side so the API key never reaches the browser. Docustay also ships a Python client, a CLI, and an MCP server that lets an AI assistant send and track documents.

by read8 min views2 publishedOct 28, 2026

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.

documents:read and documents:write. A key that starts dsk_test_ only makes practice runs: the document is emailed to you alone, and every page carries a TEST watermark. 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.

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.

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.

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.

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.

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.

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.

docustay/examples/nextjs in 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.

── more in #ai-tools 4 stories · sorted by recency
── more on @docustay 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/send-a-document-for-…] indexed:0 read:8min 2026-10-28 · —