Contracts API
Create, manage, and track contracts for signing.
https://apisign.io/apiA contract is created as a draft, sent with POST /contract/send (which charges your balance), and becomes signed when every signer has signed.
Create Contract
Where the signing link comes from. The create response does not contain a
signing link. Signers receive their link by email when you call
POST /contract/send. The link is
https://apisign.io/sign/{contract_id}/{signer_id} and only opens while the
contract's status is sent. Use the id returned for each signer to correlate
webhook events and GET /contract/get responses back to the person you created.
Contract Creation Flows
APISign supports two ways to create a contract:
Validation Rules:
- Template Contracts: Provide
template_id, plusvariablesfor each creator-completed field. Must NOT providecontentorfields. - Blank Contracts: Provide
contentandfields. Must NOT providetemplate_idorvariables.
Both flows also require:
- At least one
{{placeholder}}in the content that resolves to asignaturefield completed by the signer. - With more than one signer, a
signerFieldslist on every signer, each including a signature field no other signer has (see Signer Object).
1. Template-Based Contract Creation
Create contracts from a saved template. Each key in variables replaces the matching {{placeholder}} (case-insensitive). Signer-completed placeholders stay in the document for the signer to fill in.
{
"name": "Service Agreement - John Smith",
"template_id": "clx123abc0000abcd1234efgh",
"variables": {
"company_name": "Acme Corporation",
"client_name": "John Smith",
"hourly_rate": "$150"
},
"signers": [
{
"email": "john@example.com",
"name": "John Smith",
"signing_order": 1
}
]
}
2. Blank Contract Creation
Create one-off contracts from your own markdown. There is no variable substitution on blank contracts: write fixed values straight into content, and use placeholders only for what signers fill in.
{
"name": "Custom NDA - Jane Doe",
"content": "# Non-Disclosure Agreement\n\nThis agreement is between Acme Corporation and {{CLIENT_NAME}}.\n\n**Client Signature:** {{CLIENT_SIGNATURE}}\n\n**Signed:** {{DATE_SIGNED}}",
"fields": [
{
"name": "CLIENT_NAME",
"type": "signer_name",
"completedBy": "signer"
},
{
"name": "CLIENT_SIGNATURE",
"type": "signature",
"completedBy": "signer"
},
{
"name": "DATE_SIGNED",
"type": "timestamp",
"completedBy": "signer"
}
],
"signers": [
{
"email": "jane@example.com",
"name": "Jane Doe",
"signing_order": 1
}
]
}
A placeholder with no entry in fields is treated as signer-completed, with its type guessed from its name (see Template Variables). A field marked "completedBy": "creator" on a blank contract is never presented to a signer as an input, and its placeholder is blanked out of the final PDF.
Field Types
| Field Type | Description | Example |
|---|---|---|
signature | Signature drawing canvas | "CLIENT_SIGNATURE" |
initials | Initials drawing canvas | "CLIENT_INITIALS" |
signer_name | Filled with the signer's name | "CLIENT_NAME" |
timestamp | Filled with the time of signing | "DATE_SIGNED" |
text | Plain text input | "JOB_TITLE" |
email | Email address input | "CLIENT_EMAIL" |
phone | Phone number input | "CLIENT_PHONE" |
date | Date picker | "START_DATE" |
Each field object has name (the placeholder name without braces), type, completedBy (signer or creator), and an optional label, the prompt shown to the signer for a text field.
Signer Object
Each signer in the signers array accepts:
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Signer's email address |
name | string | Yes | Signer's full name (1-255 characters) |
signing_order | integer | Yes | Display order for the signer (1, 2, 3...). Must be unique across signers. Does not enforce signing sequence; see "Signing order" below. |
company | string | No | Signer's company name (up to 255 characters) |
mobile | string | No | Signer's mobile number. Stored only; nothing is sent to it. |
signerFields | string[] | With 2+ signers | Placeholder names this signer fills in. Required on every signer when there is more than one signer; each signer needs at least one signature field that no other signer has. A signer is shown only the fields listed here. |
Multi-signer example:
"signers": [
{ "email": "a@example.com", "name": "Ann Lee", "signing_order": 1, "signerFields": ["CLIENT_SIGNATURE", "CLIENT_NAME"] },
{ "email": "b@example.com", "name": "Bob Ray", "signing_order": 2, "signerFields": ["VENDOR_SIGNATURE"] }
]
Signing order
signing_order is required and must be unique across the signers on a contract,
but it only controls the order signers are stored and displayed in.
When you send a contract, every signer is emailed at the same time, and any
signer can complete their signature at any point. A signer with
signing_order: 3 can sign before the signer with signing_order: 1 has opened
their email. The contract moves to signed once all signers have signed,
regardless of the sequence.
Enforced sequential signing, where each signer is only notified after the
previous one completes, is not implemented. If your workflow requires it, send
separate contracts and trigger the next one from the contract_completed
webhook.
Get Contract
List Contracts
Contract Statuses
| Status | Description |
|---|---|
draft | Created but not yet sent. The only status that can be updated or deleted. |
sent | Sent to signers, awaiting signatures. |
signed | All signers have signed. |
cancelled | Cancelled via /contract/cancel, or declined by the last signer who had not yet signed. |
expired | Reserved. Contracts are not currently moved to this status automatically; see Expiration. |
The status enum also contains queued, which no endpoint currently sets.
Signer Statuses
| Status | Description |
|---|---|
pending | Has not signed yet |
signed | Has signed |
declined | Declined to sign, or was still pending when the contract was cancelled |
When a signer declines, the contract becomes cancelled and nobody else can sign it. The contract_declined webhook fires, with the signer's reason in data.metadata.declineReason; no separate contract_cancelled event is sent. Other signers keep their current status.
Email Delivery
Each signer's email_delivery describes their most recent signing request email, so a resend replaces it. It is null for a signer who hasn't been emailed, and for emails sent before delivery tracking was added. Use Contract Emails for every email on a contract, including the signed copies.
| Field | Description |
|---|---|
status | One of the statuses below |
recipient | The address it went to. In test mode, that's your organization's address, not the signer's. |
sent_at | When it was sent |
status_updated_at | When the current status was reported |
detail | Why it bounced, was deferred, rejected or failed, e.g. 550 5.1.1 mailbox does not exist. Otherwise null. |
| Status | Description |
|---|---|
sent | Handed to our email provider. No outcome reported yet. |
deferred | The recipient's server asked to try again later. Still retrying. |
delivered | The recipient's mail server accepted it. It may still have landed in spam. |
bounced | The recipient's server refused it, at send time or in a later bounce report. A sent contract's signers can't be edited, so if the address is wrong, cancel the contract and create a new one with the right address. |
rejected | Never attempted. Usually the address bounced or reported spam before, so it is no longer emailed. |
complained | The recipient marked it as spam. |
failed | The send itself failed. |
Statuses only move forward: a late deferred can't override delivered, but a later bounce report or spam complaint can. Updates usually arrive within seconds of sending. bounced, rejected and complained are final.
Update Contract
The expiration date cannot be changed after creation.
Send Contract
Billing and 402
Each send of a live contract costs your organization's per-contract price ($0.25 by default), deducted from your account balance. Creating, updating and cancelling contracts is free. If the balance is lower than the price, nothing is sent and the response is:
{
"error": "Insufficient balance. You need at least $0.25 to send a contract."
}
with HTTP status 402. Add funds in the dashboard and retry. Auto-recharge, if enabled, runs only after a successful send leaves the balance below your threshold; a 402 does not trigger it.
Resending
Calling send on a contract that is already sent resends it: every signer who hasn't signed yet is emailed again, and the send is charged again. Signers who already signed keep their status and signature and aren't emailed. If everyone has already signed, the call returns 400 and nothing is charged. The contract_resent webhook event fires instead of contract_sent.
Cancel Contract
Delete Contract
Signed PDF
There is no download endpoint. When the last signer signs, a PDF is generated and its URL is stored in the contract's final_pdf field, returned by GET /contract/get. final_pdf is an empty string until then. The PDF of a live contract carries a digital seal you can check with Verify Contract. The completion email sent to every signer and to your organization also links to the PDF.
Expiration
expires_at is set when the contract is created: the creation time plus expires_in_days (default 30, maximum 365). It cannot be changed afterwards.
After expires_at, the signing page shows an expiry message (the signing API returns 410) and signers can no longer sign or decline. /contract/send refuses a contract whose deadline has passed with 400, before anything is charged; create a new contract instead.
The status doesn't change at the deadline. A separate scheduled job may later set it to expired and emit contract_expired, but don't depend on its timing: compare expires_at with the current time to detect an expired contract.
Test Mode
Set "test_mode": true on create to rehearse the full flow:
- Sending is free and needs no balance. Your organization still needs billing set up, or send returns
400. - Signing-request emails go to you instead of the signers: the email address of the dashboard user who sent it, otherwise your organization's billing email, otherwise the organization owner. API-key and MCP sends start from the billing email. Each email is marked
[TEST]and names the signer it was meant for. If there's no address to redirect to, send returns400. - The completion email with the final PDF goes only to your organization's billing email (or the owner), never to the signers.
- The final PDF is watermarked "TEST MODE — NOT A LEGALLY BINDING DOCUMENT" and is not digitally sealed, so
/contract/verifyreports it as unsealed. - Webhooks, activity logs and audit logs work as for live contracts.
test_mode is fixed at creation. Test contracts appear in /contract/find with test_mode: true.
Contract Activity Log
Contract Audit Log
Contract Webhook Deliveries
Contract Emails
Convert Document
Verify Contract
valid is true when the contract is signed, has a seal, and the seal's certificate serial matches the platform's current signing certificate. The endpoint does not re-hash the stored PDF, so documentIntact is always true for a sealed contract.
A contract that is not signed returns 200 with valid: false, error: "Contract has not been signed" and a short contract object. A signed contract without a seal (test contracts, or a seal that failed to apply) returns valid: false, error: "Contract does not have a digital seal", seal: null, and every verification flag false.