API

Send seals from your product

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 account

Quickstart

An envelope is one document sent out for signatures. The page follows that path.

  1. Create an API key, and send it on each request.
  2. Create an envelope: who signs, in what order, and where they sign on the page.
  3. Send it. FinalSeal emails a link, or you open an in-person link on your own device.
  4. Listen for a webhook when someone signs, and when the document is sealed.

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.

Authentication

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.

  • A live key starts with seal_live_…. It sends real documents, and those seals count toward the plan.
  • A test key starts with 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.

  • Free: 10 completed seals a month.
  • Solo: 50 completed seals through the API each billing period. Seals you send from the FinalSeal website do not count toward that 50.
  • Team: 200 completed seals a month included, then $0.40 each.

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.

Create an envelope

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.
  • Each field has a 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.
  • Set 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.
  • Set 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.guid

Send it and check status

  • Leave fields off to use the boxes saved on the template. That is the next section.
  • In order (sequential): FinalSeal emails the first person who is not the sending account and who should get an email link.
  • At the same time (parallel): FinalSeal emails every such person at once.
  • People set to sign in person are not emailed. You open their link when it is their turn.

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.

Save fields on a template

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
    }
  ]
}

Webhooks

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.

  • Each message is signed with HMAC-SHA256. The signature of the raw body is in X-Seal-Signature, written as sha256=<hex>. X-Seal-Event is the event name.
  • The signing secret is shown once, when you create the webhook.
  • Every message includes test. It is true for envelopes created with a test key, and for Send test.
  • Leave every event unchecked to receive all of them.
  • On a team workspace, only an owner or admin can manage webhooks.
  • Deliveries are kept 14 days on Free, 60 days on Solo, and 120 days on Team. Retry sends the same message again, with a new signature.

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."
  }
}

Workspaces and folders

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.

Check a sealed PDF

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.

  • The response names the sender, the first recipient, and each person in signers.
  • A personal envelope includes business_name. A workspace envelope includes organization_name.
  • Upload a file on its own and FinalSeal looks for any seal with that hash.
  • If you also send 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.

Plan limits

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.

  • Free or Solo, past the limit: HTTP 402, with code set to quota_exceeded. The next send waits until the next period.
  • A workspace without an active Team plan: HTTP 402, with code set to team_plan_required.
  • Team, past the 200 included seals: the send still works. Each extra completed seal is $0.40.

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.