Contracts API

Create, manage, and track contracts for signing.

Base URL:https://apisign.io/api

A 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, plus variables for each creator-completed field. Must NOT provide content or fields.
  • Blank Contracts: Provide content and fields. Must NOT provide template_id or variables.

Both flows also require:

  • At least one {{placeholder}} in the content that resolves to a signature field completed by the signer.
  • With more than one signer, a signerFields list 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 TypeDescriptionExample
signatureSignature drawing canvas"CLIENT_SIGNATURE"
initialsInitials drawing canvas"CLIENT_INITIALS"
signer_nameFilled with the signer's name"CLIENT_NAME"
timestampFilled with the time of signing"DATE_SIGNED"
textPlain text input"JOB_TITLE"
emailEmail address input"CLIENT_EMAIL"
phonePhone number input"CLIENT_PHONE"
dateDate 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:

FieldTypeRequiredDescription
emailstringYesSigner's email address
namestringYesSigner's full name (1-255 characters)
signing_orderintegerYesDisplay order for the signer (1, 2, 3...). Must be unique across signers. Does not enforce signing sequence; see "Signing order" below.
companystringNoSigner's company name (up to 255 characters)
mobilestringNoSigner's mobile number. Stored only; nothing is sent to it.
signerFieldsstring[]With 2+ signersPlaceholder 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

StatusDescription
draftCreated but not yet sent. The only status that can be updated or deleted.
sentSent to signers, awaiting signatures.
signedAll signers have signed.
cancelledCancelled via /contract/cancel, or declined by the last signer who had not yet signed.
expiredReserved. Contracts are not currently moved to this status automatically; see Expiration.

The status enum also contains queued, which no endpoint currently sets.

Signer Statuses

StatusDescription
pendingHas not signed yet
signedHas signed
declinedDeclined 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.

FieldDescription
statusOne of the statuses below
recipientThe address it went to. In test mode, that's your organization's address, not the signer's.
sent_atWhen it was sent
status_updated_atWhen the current status was reported
detailWhy it bounced, was deferred, rejected or failed, e.g. 550 5.1.1 mailbox does not exist. Otherwise null.
StatusDescription
sentHanded to our email provider. No outcome reported yet.
deferredThe recipient's server asked to try again later. Still retrying.
deliveredThe recipient's mail server accepted it. It may still have landed in spam.
bouncedThe 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.
rejectedNever attempted. Usually the address bounced or reported spam before, so it is no longer emailed.
complainedThe recipient marked it as spam.
failedThe 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 returns 400.
  • 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/verify reports 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.