Create documents from your templates, send them, follow every signer and download the signed PDF, over a plain JSON REST API. Webhooks tell your systems the moment something is signed. Included on every plan, with no limit on documents.
Each signer's status, when they viewed and signed, what they filled in, and the audit trail. Once it's completed, the PDF is the signed copy with its Certificate of Completion.
Send the key as a bearer token on every request. Give each integration its own key, so you can see when each was last used and revoke one without touching the others.
Authorization: Bearer tsk_…
One company per key
A key reaches only its own company's templates and documents, the same ones its person sees in the app.
It acts as its person
Documents it sends come from the person who created the key, and the audit trail names them. Their own signature goes only to them.
The same rules as the app
Sending needs an active account; a suspended company or a person whose access was turned off can't use their keys.
Stored as a hash
Tally Sign keeps only a SHA-256 hash of each key. Lost one? Revoke it and create another.
Revoke any time
Settings → AI & API lists every key with when it was last used. Revoking one stops it, and its webhooks, at once.
HTTPS only
Send the key in the Authorization header over HTTPS. Never put it in a web page or a mobile app.
Reference every endpoint
Account
The key itself.
GET/api/v1/me
Check your API key
Which company the key works for and whose key it is. A good first call, and the test Zapier runs when you connect.
Makes a draft from a template: name who signs for each role, fill in variables, and choose how it's sent. With send: true it goes out for signature at once; if anything is missing, nothing is created and the answer (not_ready) lists what. Your own side fills from the API key's person and company profile.
Body
templateIdrequired
string
The template to start from (GET /templates).
title
string
Document title (default: the template's).
variables
object
Values for the template's {{variables}}, by key.
recipients
RecipientInput[]
signingOrder
"parallel" | "sequential"
sequential asks signers one after another by order. Default: everyone at once.
expiresInDays
integer
Days until the signing links stop working (default 60). 1 to 365.
subject
string
Email subject of the signing request.
message
string
Personal note in the signing request email.
send
boolean
Send it for signature right away. If anything is missing, nothing is created and the answer lists what.
Each recipient
rolerequired
string
A role from the template (roles[].role), or a new one to add a signer.
name
string
email
string
title
string
company
string
order
integer
Signing order when signingOrder is sequential. 0 to 100.
Status, every recipient with when they viewed and signed, what each signer filled in, and the audit trail. A signer's fields and fieldValues fill in once they sign (until then they're empty: entries before signing aren't agreed to), and fieldValues on the document merges every signer's. Files signers uploaded are listed with a link to download them with your API key.
Emails each signer their own signing link (the first ones only, when signing in order). Answers not_ready with the list of what's missing when it can't be sent yet.
Path
idrequired
string
Body
signingOrder
"parallel" | "sequential"
sequential asks signers one after another by order. Default: everyone at once.
expiresInDays
integer
Days until the signing links stop working (default 60). 1 to 365.
Cancels a document that's awaiting signatures. Its signing links stop working, and signers who had it are told (unless your company turned that email off).
Path
idrequired
string
Body
reason
string
Recorded in the audit trail and included in the void notice to signers.
Takes an Idempotency-Key header.
Errors: not_found, conflict.
Request
curl -X POST "https://app.tallysign.com/api/v1/documents/$ID/void" \
-H "Authorization: Bearer $TALLY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"reason": "Replaced by the revised NDA."
}'
A completed document's signed PDF: every page as signed, its exhibits, and the Certificate of Completion with the audit trail at the end. Before it's completed, a preview of the document as it stands (add preview=true to always get the preview). The answer may be a redirect to a short-lived file link: follow it (curl -L).
A file a signer uploaded into a File upload field, once they've signed with it: a PDF, or a JPEG or PNG image, under the name it had. Its id and url are in the signer's fields (files). The answer may be a redirect to a short-lived file link: follow it (curl -L).
For signing inside your own product or in person: the link a signer would get by email. It works only while the document is out for signature and it's that signer's turn. Your company's own signer's link goes only to the API key's person when they are that signer. Each link handed out is recorded in the audit trail.
Path
idrequired
string
recipientIdrequired
string
Body
inPerson
boolean
Hand your device to the signer: the link opens the in-person hand-off, valid 2 hours, and the certificate records the API key's person as host.
Tally Sign posts each event you pick to your URL as JSON, signed with the subscription's secret. Failed deliveries are retried for about a day. Answering 410 Gone removes the subscription. Only company owners' and admins' keys can subscribe.
What signers filled in typed values, ready for a spreadsheet
Every recipient on a document, and every webhook about one, carries what that signer entered: each field with its label and typed value, the same as one flat object for no-code tools, and the files they uploaded.
A signer's fields and fieldValues fill in once they sign. Until then they're empty: what someone types before signing isn't agreed to.
Values are typed: a number as a number, a checkbox as true or false, a dropdown or radio as the option chosen, a date they picked and the date they signed as YYYY-MM-DD, anything else as text. Left empty: null.
Signatures and initials aren't listed (signedAt says they signed), nor conditional fields that stayed hidden.
fieldValues keys are the field's label as you wrote it in the template, or its type (Text) when it has none. A label used twice by one signer becomes Label (2), Label (3) in page order.
The document's fieldValues merges every signer's. A key two signers share is prefixed by the role label: Counterparty: Company. Keys come from the document as sent, so they stay the same as more people sign. Read values by key: the order of an object's keys isn't guaranteed (fields is in page order).
File uploads give the file's name as the value, and the file under files with its size, type and SHA-256. Download it with your API key from url.
date is the date they signed; dateInput a date they picked.
labelrequired
string
The field's label as the sender wrote it; its type (e.g. Text) when it has none.
keyrequired
string
Its key in fieldValues: the label, made unique per signer as Label (2), Label (3) in page order.
valuerequired
string | number | boolean | null
Typed: a number as a number, a checkbox as true or false, a dropdown or radio as the option chosen, date and dateInput as YYYY-MM-DD, a file upload as the file's name, anything else as text. null when left empty.
files
SignerFile[]
File upload fields: the file they signed with.
Each file
idrequired
string
filenamerequired
string
The name the signer's file had.
sizerequired
integer
Bytes. -9007199254740991 to 9007199254740991.
contentTyperequired
string
application/pdf, image/jpeg or image/png (checked from the file itself).
sha256required
string
SHA-256 of the file, hex: the fingerprint on the Certificate of Completion.
uploadedAtrequired
date-time
When the signer uploaded it.
urlrequired
string
Download it with your API key (GET /documents/{id}/files/{fileId}). Not a public link.
A signer who has signed (part of their fields, and all of fieldValues)
Subscribe a URL to the events you care about with POST /api/v1/webhooks. Tally Sign posts each one as JSON, signed with the subscription's secret. Automations can send the same payload to a URL too, without code.
Events
document.sent
A document was sent for signature (again, when an edited version is sent).
recipient.viewed
A signer opened the document and interacted with it for the first time (link scanners don't count).
recipient.signed
A signer signed. Sent for every signer, your own side included.
document.completed
Everyone has signed. The signed PDF is ready to download.
document.declined
A signer declined to sign. recipient.declinedReason says why.
document.voided
A document awaiting signatures was voided.
document.expired
The signing links expired before everyone signed.
Delivery
Answer with any 2xx within 10 seconds. Do the slow work afterwards.
Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours, then dropped. The id stays the same on every retry, so you can ignore repeats.
Answering 410 Gone deletes the subscription.
Only public https addresses receive events. Redirects aren't followed.
Headers: X-Tally-Event, X-Tally-Delivery and X-Tally-Signature.
Admins see every subscription in Settings → AI & API and can remove any of them. Revoking a key removes its subscriptions.
X-Tally-Signature is sha256= followed by the HMAC-SHA256 of the raw body with your subscription's secret, in hex. Compute it over the bytes you received, before parsing, and compare in constant time.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received (a Buffer or string), before JSON parsing.
function isFromTallySign(rawBody, signatureHeader, secret) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const got = Buffer.from(signatureHeader ?? "");
return got.length === expected.length && timingSafeEqual(got, Buffer.from(expected));
}
The HTTP status says what kind of problem it is, and the body says which one, in words you can show a person.
{
"error": {
"code": "not_ready",
"message": "It can't be sent yet: Dana Whitfield: add a valid email address.",
"details": {
"issues": [
"Dana Whitfield: add a valid email address."
]
}
}
}
Code
Status
Meaning
invalid_request
400
The request isn't valid: a missing or wrong field (details.fields says which), bad JSON or an unknown filter.
unauthorized
401
No API key, or it isn't valid any more (revoked, or its person was removed).
billing_paused
402
Sending is paused for your company's account. Drafts can still be made; an admin can resolve it in Settings → Billing.
forbidden
403
The key can't do this (e.g. only owners' and admins' keys subscribe to webhooks), or the account is suspended.
not_found
404
No such document, template, signer or subscription in your company.
conflict
409
The document isn't in a state that allows it (e.g. voiding a completed document), or the same Idempotency-Key is still being worked on.
not_deletable
409
Only drafts and voided documents can be deleted.
not_ready
422
It can't be sent yet. details.issues lists what's missing (a signer's email, a variable).
idempotency_key_reused
422
This Idempotency-Key was already used for a different request.
too_soon
429
That signer was reminded within the last hour.
rate_limited
429
Over 300 requests a minute for this key. Wait for Retry-After seconds.
server_error
500
Something went wrong on our side. It's logged; try again.
Limits and conventions so retries and paging just work
300 requests a minute
Per key. Every answer has X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; past the limit you get 429 with Retry-After.
Pages with a cursor
Lists answer with data, hasMore and nextCursor. Pass nextCursor as cursor for the next page. Documents come most recently changed first.
Safe retries
Send an Idempotency-Key header (a UUID) when creating, sending, reminding, voiding or subscribing. A retry within 24 hours gets the first answer back.
JSON in, JSON out
Field names are camelCase. Times are ISO 8601 in UTC. Unknown fields may be added to answers, so don't fail on them.
No limit on documents
Every plan sends as many documents as you need. The rate limit stops runaway loops, not real work.
Versioned
Everything lives under /api/v1. Changes that could break your code come as a new version, never in this one.
No code
Zapier and Make
Zapier and Make work with Tally Sign today through webhooks and the REST API: catch an event when a document is signed, or call the API to send one.
In Zapier, use Webhooks by Zapier: Catch Hook as the trigger, and subscribe its URL to the events you want. To send a document, call POST /api/v1/documents with Webhooks by Zapier: Custom Request and your API key.
AI apps
ChatGPT, Claude and MCP
For AI apps there's a built-in MCP server at /api/mcp. ChatGPT and Claude connect with a sign-in; Claude Code, Cursor and other MCP clients use the same API keys as this API.
No. It's part of both plans, and API calls, documents and webhooks aren't metered.
Can I make templates through the API?+
Not today. Make and edit templates in Tally Sign, where you see the page as it will print, then create documents from them through the API. AI apps connected over MCP can draft documents block by block.
Can my customers sign inside my own product?+
Yes. Create and send the document, then ask for that signer's signing link and open it in their browser. Each link handed out is recorded in the audit trail. Keep the link to that person: whoever opens it can sign as them.
Is a document sent through the API the same as one sent from the app?+
Yes. The same checks, emails, reminders, automations, audit trail and Certificate of Completion.
Where's the OpenAPI document?+
At https://app.tallysign.com/api/v1/openapi.json. It's OpenAPI 3.1, generated from the same definitions the API checks requests with, so it's always current.
How do I test without emailing real people?+
Use your own addresses as the signers while you build. Every email and signature is real, so use documents you're happy to void, and delete drafts and voided ones when you're done.
Step-by-step guides
Tally Sign Support walks through each part with screenshots of the real product.