API
Create a test key, send a document for signature, and get a notice when someone signs. What a live key can send is on the Pricing page. Request and response fields for every call are in the API reference.
Create an accountAn envelope is one document sent out for signatures. The page follows that path.
Between sending and webhooks, you can save the signature boxes on a template instead of sending them every time. After webhooks, the page covers workspaces and folders, checking a sealed PDF, and plan limits.
Send the API key in the X-API-Key header on each request. The full key is shown once, when you create it. After that, you only see the prefix.
seal_live_…. It sends real documents, and those seals count toward the plan.seal_test_…. Use it while you build. Test seals are not counted, no email is sent, and the finished PDF is watermarked TEST — NOT LEGALLY BINDING. A test key only works with test envelopes. A live key cannot see those envelopes.A live seal counts on the FinalSeal account that created the key. A seal counts when the document is completed.
See Pricing. Past the Free or Solo limit, send returns HTTP 402. That case is under Plan limits, below. The account owner changes the plan in Settings on the FinalSeal website.
On a team workspace, only an owner or admin can create or list keys. A key acts as the person who created it.
Sign in to manage API keys.
Creating an envelope saves a draft. Nothing is emailed until you send it. Call POST /api/v1/envelopes/ on this site. The sample below uses http://localhost. Swap that host for the site you are calling.
recipients is 1 to 10 people. Each person needs a name and an email. For one person, recipient_email and recipient_name are enough.routing is sequential when they sign in order, or parallel when any of them can sign before the others.signer_index, starting at 1, in the same order as recipients. x_coord, y_coord, width, and height are fractions of the page, measured from the top left. 0.1 is 10% of the page.is_sender: true on the person who is the FinalSeal account sending the document. They are not emailed a link. They sign with POST /envelopes/{guid}/sign/ or on the FinalSeal website, when it is their turn.delivery: "kiosk" when that person will sign on a device you hand them. They still need a name and an email. They are not emailed a link. The finished PDF is emailed to that address.Two people, signing in order. The tenant signs on a device you hand them. The landlord is your FinalSeal account and signs second.
curl -X POST http://localhost/api/v1/envelopes/ \
-H "X-API-Key: seal_live_..." \
-H "Content-Type: application/json" \
-d '{
"template_guid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"routing": "sequential",
"recipients": [
{"email": "tenant@example.com", "name": "Tenant", "delivery": "kiosk"},
{"email": "landlord@example.com", "name": "Landlord", "is_sender": true}
],
"subject": "Please sign the lease",
"fields": [
{
"page_number": 1,
"x_coord": 0.1,
"y_coord": 0.7,
"width": 0.3,
"height": 0.08,
"field_type": "signature",
"label": "Tenant",
"signer_index": 1
},
{
"page_number": 1,
"x_coord": 0.55,
"y_coord": 0.7,
"width": 0.3,
"height": 0.08,
"field_type": "signature",
"label": "Landlord",
"signer_index": 2
}
]
}'The same request in JavaScript. The response includes guid, which the later calls use.
const res = await fetch("/api/v1/envelopes/", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.SEAL_API_KEY,
},
body: JSON.stringify({
template_guid: templateGuid,
routing: "sequential",
recipients: [
{ email: "tenant@example.com", name: "Tenant", delivery: "kiosk" },
{ email: "landlord@example.com", name: "Landlord", is_sender: true },
],
fields: [/* signer_index 1, 2, ... */],
}),
});
const envelope = await res.json();
// envelope.guidfields off to use the boxes saved on the template. That is the next section.sequential): FinalSeal emails the first person who is not the sending account and who should get an email link.parallel): FinalSeal emails every such person at once.After that, these calls cover status, links, and signing.
POST/api/v1/envelopes/{guid}/send/
Start signing. The envelope leaves draft, and FinalSeal emails a link to each person who should get one.
GET/api/v1/envelopes/
List envelopes you can see. Filter with scope, status, q, completed_after, limit, and cursor. Those filters are explained under Workspaces and folders.
GET/api/v1/envelopes/{guid}/
Read one envelope: its status, the people on it, and the fields.
GET/api/v1/envelopes/{guid}/events/
Read what has happened: sent, viewed, signed, completed, declined, or voided.
GET/api/v1/envelopes/{guid}/pdf/
Download the PDF. After the last signature, this file is the sealed document.
GET/api/v1/envelopes/{guid}/signing-url/
Get the link that would have been emailed, for someone waiting on email. Add recipient_guid to choose the person.
GET/api/v1/envelopes/{guid}/recipients/{rguid}/kiosk-url/
Get a link to open on your own device so that person can sign in person, when it is their turn. Only the account that sent the envelope can request it. Anyone else gets 403. Opening the link in a browser where you are signed in locks that browser until the kiosk PIN is entered. Requesting the link with an API key does not lock the browser.
PATCH/api/v1/envelopes/{guid}/recipients/{rguid}/
Switch one person between an email link and signing in person. Send delivery as email or kiosk. They must not have signed yet, and they cannot be the sending account. If people sign in order, it has to be their turn.
POST/api/v1/envelopes/{guid}/resend/
Email the signing link again to someone who is waiting on email. Add recipient_guid to choose the person.
POST/api/v1/envelopes/{guid}/void/
Cancel a draft, or an envelope that is still waiting for signatures.
POST/api/v1/envelopes/{guid}/decline/
The sending account declines, when it is their turn. The document is not sealed.
POST/api/v1/envelopes/{guid}/sign/
The sending account submits their own signature, when it is their turn. Call it with an API key, or from a signed-in browser. The body is a fields list. Each entry has field_guid, plus value, image_base64, or both.
POST/api/v1/sign/{token}/submit
The person who opened the signing link submits their signature.
POST/api/v1/sign/{token}/decline
The person who opened the signing link declines. The document is not sealed.
POST/api/v1/sign/{token}/renew/
The signing link has expired. Email a new one.
If every send uses the same signature boxes, save them once on the template. You can then create an envelope without a fields list. Coordinates work the same way as on an envelope: fractions of the page, from the top left.
GET/api/v1/templates/{guid}/fields/
Read the fields saved on a template.
PUT/api/v1/templates/{guid}/fields/
Replace that list. Send every field, including the ones you did not change.
One signature box for the first signer. Send the whole list each time.
{
"fields": [
{
"page_number": 1,
"x_coord": 0.1,
"y_coord": 0.7,
"width": 0.3,
"height": 0.08,
"field_type": "signature",
"signer_index": 1
}
]
}A webhook is an HTTPS URL on the public internet. When something happens to an envelope, FinalSeal sends that URL a JSON message. FinalSeal checks the URL again at delivery time, and does not call it if it no longer points at the public internet.
X-Seal-Signature, written as sha256=<hex>. X-Seal-Event is the event name.test. It is true for envelopes created with a test key, and for Send test.Send test posts a signed webhook.ping message with test set to true, so you can check the address without sending a real document. That call is POST /api/v1/webhooks/{guid}/ping/.
Sign in to manage webhooks.
FinalSeal sends these events:
envelope.sent — the envelope was sent.envelope.signer_signed — one person signed. This is sent once per signer.envelope.completed — the last person signed, and the PDF is sealed. The message includes signers and document_sha256.envelope.declined — someone declined. The PDF is not sealed.webhook.ping — sent by the Send test button. It is not one of the events you check above.When the envelope is sent:
{
"event": "envelope.sent",
"delivered_at": "2026-01-01T11:59:00.000000+00:00",
"test": false,
"data": {
"envelope_guid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"status": "pending",
"recipient_email": "tenant@example.com",
"invited": ["tenant@example.com"]
}
}When one person signs:
{
"event": "envelope.signer_signed",
"delivered_at": "2026-01-01T11:59:30.000000+00:00",
"test": false,
"data": {
"envelope_guid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"status": "pending",
"recipient_guid": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
"recipient_email": "tenant@example.com",
"recipient_name": "Tenant",
"routing_order": 1,
"delivery": "kiosk"
}
}When the last person signs and the PDF is sealed:
{
"event": "envelope.completed",
"delivered_at": "2026-01-01T12:00:00.000000+00:00",
"test": false,
"data": {
"envelope_guid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"status": "completed",
"completed_at": "2026-01-01T12:00:00+00:00",
"document_sha256": "abcdef...",
"recipient_email": "tenant@example.com",
"recipient_name": "Tenant",
"signers": [
{
"name": "Tenant",
"email": "tenant@example.com",
"routing_order": 1,
"signed_at": "2026-01-01T11:59:30+00:00",
"is_sender": false
},
{
"name": "Landlord",
"email": "landlord@example.com",
"routing_order": 2,
"signed_at": "2026-01-01T12:00:00+00:00",
"is_sender": true
}
]
}
}When you use Send test:
{
"event": "webhook.ping",
"delivered_at": "2026-01-01T12:00:00.000000+00:00",
"test": true,
"data": {
"ping": true,
"detail": "Test delivery from FinalSeal. Not a real envelope."
}
}Creating a workspace on the FinalSeal website moves that account's templates, envelopes, folders, API keys, and webhooks into the workspace.
When you list templates or envelopes, choose which set of files:
scope=personal — that person's own files.scope=org — the workspace. In responses, the workspace name is organization_name.You can also narrow the envelope list:
status — draft, pending, completed, void, or declined.q — words from the subject, a name, or an email.completed_after — only envelopes sealed at or after this time.limit — how many to return, from 1 to 100.cursor — the next page. Read it from the X-Next-Cursor response header, and send it together with limit.To upload a template into the workspace, post it to POST /api/v1/templates/ with form fields scope and, if you want a folder, folder_guid. Only a workspace owner or admin can upload into the workspace.
To move a template into a folder, or between personal files and the workspace, send PATCH /api/v1/templates/{guid}/ with folder_guid, scope, or both.
Folders are a single level. Create, list, update, and delete them at /api/v1/folders/.
A workspace envelope shows organization_name on the certificate, and again when someone checks the sealed PDF. A personal envelope shows business_name from the sender's profile, when that name is set. The sender is the person who created the envelope.
Anyone can check a finished PDF. No API key is required. The same check is on the website: Verify a document.
The proof is the SHA-256 hash of the sealed PDF, byte for byte. If the file changes, the hash changes, and it no longer matches.
signers.business_name. A workspace envelope includes organization_name.envelope_guid, the file has to be that envelope's sealed PDF.GET/api/v1/verify/{guid}/
Look up one seal by its id. No API key.
POST/api/v1/verify/
Upload a PDF as multipart form data. The file field is the PDF. envelope_guid is optional. No API key.
Limits are checked when a live key calls POST /envelopes/{guid}/send/. Test keys are not counted. A document that was already sent can still be finished. That does not return 402.
code set to quota_exceeded. The next send waits until the next period.code set to team_plan_required.This API cannot change the plan. The account owner subscribes, manages cards, and changes the plan in Settings on the FinalSeal website.
Every id in this API is a UUID, in a field named guid.
The API reference lists every endpoint, with the request and response fields.