Developer docs

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

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 are null.
  • 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.

  1. The agent drafts an envelope and calls request_send (or request_void, request_correct).
  2. 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.
  3. 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.
  4. You approve with a passkey, with user verification (Face ID, Touch ID, your device PIN or a security key), or you reject the request.
  5. 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.