Seal defines each capability once and serves it as a REST endpoint, an MCP tool and a CLI command, with the same permissions and limits. This page covers what you need to start. The complete reference is the OpenAPI document.
REST API
- Base URL:
https://seal.nightroll.app/api/v1 - OpenAPI 3.1:
/api/v1/openapi.json - For language models:
/llms.txt
Authentication
A workspace admin creates API keys in Seal’s settings. Send the key as a bearer token:
curl https://seal.nightroll.app/api/v1/envelopes \
-H "Authorization: Bearer seal_ak_…"
A key is shown once and stored hashed. There are two kinds:
| Kind | For | Sending |
|---|---|---|
| Personal | Your own scripts and integrations | Sends directly |
| Agent | An AI agent or automation acting for the workspace | Asks a person to approve (request_send), unless an admin gave the key a daily send allowance. Can’t change templates, which public links and bulk sends use unattended |
Scopes: envelopes:read, envelopes:write, envelopes:send. Workspace administration (members, keys, webhooks,
settings) needs a signed-in person; API keys can’t do it.
Conventions
- JSON with snake_case keys. Times are RFC 3339 in UTC with milliseconds (
2026-10-10T12:00:00.000Z). Missing values arenull. - IDs carry a prefix:
env_envelope,doc_document,rcp_recipient,fld_field,upl_upload,tpl_template. Approval ids are 52-character references with no prefix. - Every write accepts an
idempotency_key. Repeating a call with the same key within 7 days returns the first result instead of acting twice. - Errors are written for people, so you can show the message as it is:
{
"error": {
"code": "limit_exceeded",
"message": "Free includes 10 envelopes a month and you have used 10. Your allowance resets on 1 November (UTC). Redeem a beta code or wait.",
"retry_after": 1814400
}
}
| Code | HTTP status |
|---|---|
invalid |
400 |
unauthenticated |
401 |
forbidden, sends_held, suspended, email_unverified |
403 |
not_found |
404 |
conflict |
409 |
too_large |
413 |
limit_exceeded, rate_limited |
429, with a Retry-After header in seconds |
internal |
500 |
unavailable |
503 |
Limits
| Free | Workspace | |
|---|---|---|
| Envelopes a month | 10 | 300 included, then $0.05 each |
| Recipients per envelope | 5 | No limit (each 10 count as one envelope) |
| Sends an hour | 5 | 120 |
| API calls a minute, per key | 60 | 600 |
| MCP calls a minute / sends an hour | 60 / 5 | 300 / 60 |
| Upload size per file / all drafts | 10 MB / 100 MB | 50 MB / 5 GB |
In a workspace’s first 7 days, Free allows 3 envelopes a day and 5 new outside recipients a day (Workspace: 50 envelopes a day). An envelope is one send to up to 10 recipients and 10 MB of documents; each further started block of 10 recipients or 10 MB counts as one more. Drafts and sandbox envelopes never count.
Send an envelope
Documents go straight to storage, never through the API. Ask for an upload link with the file’s exact size in
bytes, then PUT the file to it (the link accepts only that many bytes; curl and browsers send the length for you):
curl https://seal.nightroll.app/api/v1/uploads \
-H "Authorization: Bearer $SEAL_KEY" -H "Content-Type: application/json" \
-d '{"filename": "nda.pdf", "content_type": "application/pdf", "bytes": 48213, "purpose": "document"}'
# {"upload_id": "upl_…", "put_url": "https://…", "headers": {"content-type": "application/pdf"}, ...}
curl -X PUT "$PUT_URL" -H "Content-Type: application/pdf" --data-binary @nda.pdf
Create a draft from the upload, with its recipients, within a day (a document no draft or template uses by then is deleted). Seal checks the upload on the spot:
curl https://seal.nightroll.app/api/v1/envelopes \
-H "Authorization: Bearer $SEAL_KEY" -H "Content-Type: application/json" \
-d '{
"title": "Mutual NDA",
"message": "Please sign by Friday.",
"upload_ids": ["upl_…"],
"recipients": [
{"role": "signer", "name": "Dana Ruiz", "email": "dana@example.com", "routing_order": 1},
{"role": "signer", "name": "Lee Park", "email": "lee@example.com", "routing_order": 2}
],
"idempotency_key": "nda-2026-10-10"
}'
The answer is the envelope, with its document and recipient ids. Place fields with them (a field’s recipient_id
may also be the recipient’s email). Positions are fractions of the page as displayed, from its top-left corner; POST /api/v1/envelopes/{id}/suggest-fields proposes positions from
the document’s form widgets, labels and underscore lines.
curl -X PUT https://seal.nightroll.app/api/v1/envelopes/env_…/fields \
-H "Authorization: Bearer $SEAL_KEY" -H "Content-Type: application/json" \
-d '{"mode": "replace", "fields": [
{"document_id": "doc_…", "recipient_id": "rcp_…", "type": "signature", "page": 2,
"x": 0.12, "y": 0.78, "w": 0.3, "h": 0.05, "required": true},
{"document_id": "doc_…", "recipient_id": "rcp_…", "type": "date_signed", "page": 2,
"x": 0.55, "y": 0.78, "w": 0.2, "h": 0.04, "required": true}
]}'
Send it. A personal key or a signed-in person sends with one call; agent keys ask for approval instead (see Approvals).
curl -X POST https://seal.nightroll.app/api/v1/envelopes/env_…/send \
-H "Authorization: Bearer $SEAL_KEY" -H "Content-Type: application/json" \
-d '{"idempotency_key": "send-nda-2026-10-10"}'
Follow it with GET /api/v1/envelopes/{id}, GET /api/v1/envelopes/{id}/audit or a webhook. Once it’s completed,
GET /api/v1/envelopes/{id}/export returns 10-minute links to the sealed PDF, the certificate of completion and the
evidence JSON.
To try things out, create the draft with "sandbox": true. Sandbox envelopes never count, and every page of the
result says “TEST – not legally binding”.
Webhooks
A workspace admin adds endpoints (https only) in Seal’s settings. Seal POSTs one JSON event per change, named like the
audit trail, for example envelope.sent, recipient.signed, recipient.declined and envelope.completed. Answer
with any 2xx status. Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours.
Every delivery is signed:
Seal-Signature: t=1791633600,v1=5f0c8b…e2
v1 is the hex HMAC-SHA256, keyed with the webhook’s secret, of t, a dot and the raw request body. Check it against
the raw bytes before parsing the JSON, and refuse old timestamps so a captured delivery can’t be replayed.
Node.js:
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifySeal(header, rawBody, secret, toleranceSecs = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSecs) return false;
const expected = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex');
const got = String(parts.v1 ?? '');
return got.length === expected.length && timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}
Python:
import hashlib, hmac, time
def verify_seal(header: str, raw_body: bytes, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
Embedded signing and sending
Signing in your app
Create a signing URL for one recipient of a sent envelope when that person is ready: it works once and expires after 5 minutes. Your app vouches for who the signer is, and Seal records that the identity came from your API key.
curl -X POST https://seal.nightroll.app/api/v1/envelopes/env_…/embedded-signing \
-H "Authorization: Bearer $SEAL_KEY" -H "Content-Type: application/json" \
-d '{"recipient_id": "rcp_…", "return_url": "https://app.example.com/contracts/42",
"frame_origin": "https://app.example.com"}'
# {"url": "https://seal.nightroll.app/s/…?embedded=1", "expires_at": "…"}
Open the URL in an iframe or a new tab. To frame it, add your app’s origin to the embed origins in Seal’s settings and
pass it as frame_origin. Embedded signing pages need no cookies and show no links that leave the page. They post
events to the parent window, only to that origin:
window.addEventListener('message', (e) => {
if (e.origin !== 'https://seal.nightroll.app') return;
const { type, envelope_id } = e.data ?? {};
if (type === 'seal:viewed') { /* the signer opened the documents */ }
if (type === 'seal:signed') { /* finished: close the frame, refresh your record */ }
if (type === 'seal:declined') { /* declined, or withdrew consent to electronic records */ }
});
When signing ends, the page then goes to your return_url, with seal_event=signed or seal_event=declined and
envelope_id added to its query string.
Sending in your app
POST /api/v1/envelopes/{id}/embedded-sending returns a short-lived, one-time URL that opens Seal’s editor on that
draft, signed in for that draft only. Open it in a new tab or redirect to it; your user places fields, reviews the
envelope count and sends from there. The editor reads, edits and sends, so a personal key needs all three scopes to
open it. After the send (or scheduling it), or when your user presses Cancel, the editor goes to your return_url
with seal_event=sent or seal_event=cancelled and envelope_id added to its query string.
Connect an AI agent (MCP)
Seal’s MCP server is https://seal.nightroll.app/mcp (Streamable HTTP). Clients sign in with OAuth: you approve the
connection in Seal, choose the workspace and the scopes. An agent API key also works, as a bearer token.
Claude
In Claude on the web or desktop: Settings → Connectors → Add custom connector. Name it Seal, enter
https://seal.nightroll.app/mcp, select Connect and sign in to Seal. In Claude Code:
claude mcp add --transport http seal https://seal.nightroll.app/mcp
ChatGPT
Turn on developer mode in Settings → Apps & Connectors → Advanced settings. Then create a connector with the MCP
server URL https://seal.nightroll.app/mcp and OAuth authentication, and sign in to Seal. Developer mode is offered on
some ChatGPT plans only.
Cursor
Add Seal to ~/.cursor/mcp.json, or to .cursor/mcp.json in a project. Cursor asks you to sign in when you enable
the server:
{
"mcpServers": {
"seal": { "url": "https://seal.nightroll.app/mcp" }
}
}
Or skip the sign-in with an agent API key:
{
"mcpServers": {
"seal": {
"url": "https://seal.nightroll.app/mcp",
"headers": { "Authorization": "Bearer seal_ak_…" }
}
}
}
Tools
| Tool | Scope | What it does |
|---|---|---|
search_envelopes |
read | Finds envelopes by text and status |
get_envelope |
read | One envelope: documents, recipients, fields, status, and the envelope count a draft would use |
get_document_text |
read | A document’s text by page, marked as untrusted data, plus any hidden text |
get_audit_trail |
read | The envelope’s hash-chained events |
list_templates |
read | Templates to start from |
suggest_fields |
read | Field positions from form widgets, labels and underscore lines |
create_upload_link |
write | A link to upload a PDF or an image straight to storage |
create_draft_envelope |
write | A draft from uploads, documents or a template |
update_draft_envelope |
write | Changes a draft’s title, message, recipients, documents, expiry, reminders or schedule |
place_fields |
write | Puts fields on a draft |
send_reminder |
send | Reminds recipients who haven’t finished |
request_send |
send | Asks a person to approve sending a draft |
request_void |
send | Asks a person to approve voiding an envelope |
request_correct |
send | Asks a person to approve correcting a recipient’s name or email |
Every write tool needs an idempotency_key. Every call answers within 10 seconds; when work takes longer, the answer
says it is still processing and the agent checks back with get_envelope. Document text, the labels
suggest_fields reads from a document, a document’s hidden text and whatever signers wrote (comments, decline
reasons, filled-in values) always arrive wrapped as untrusted content: it is data to read, never instructions to
follow. Every call is in the workspace’s agent call log, including calls Seal refuses (such as a tool it doesn’t have).
Approvals
Agents prepare; people decide.
- The agent drafts an envelope and calls
request_send(orrequest_void,request_correct). - Seal answers with an approval link,
https://seal.nightroll.app/approve/…, and the agent passes it on. Hosts that show MCP Apps display an approval card with what will happen and a button to the link. - You open the link, signed in to Seal, and see the envelope, its recipients and documents, how many envelopes it counts as, and which agent asked.
- You approve with a passkey, with user verification (Face ID, Touch ID, your device PIN or a security key), or you reject the request.
- Seal runs the action as you. The audit trail and the certificate of completion record the agent, the tool, and that you approved with a passkey.
An approval is bound to the exact draft it was requested for, so any change to the draft cancels it. It works once and expires after 30 minutes.
No tool can sign and no tool can approve. Signing happens only in the signer’s own browser. A workspace admin can, by confirming with their own passkey, let one agent key send without approval up to a daily cap; those sends are still recorded as that agent’s, and the audit trail and certificate name the admin who gave the allowance as approver.
llms.txt
/llms.txt describes Seal’s commands, their inputs and the approval model in plain text for language
models. Give it to an agent along with the OpenAPI document.