API Documentation

Create and manage contracts programmatically with the CanUSign API.

Authentication

All API requests require authentication using a Bearer token. Create an API key in your Settings.

Authorization: Bearer canu_your_api_key_here

Rate Limiting

The API is rate limited to 60 requests per minute per API key.

Rate limit headers are included in all responses:

  • X-RateLimit-Limit - Max requests per window
  • X-RateLimit-Remaining - Remaining requests
  • X-RateLimit-Reset - Window reset time (ISO 8601)

Over the limit you get 429 with code: "RATE_LIMITED" and a Retry-After header in seconds.

Base URL

https://canusign.com/api/v1

Endpoints

POST/contracts

Create a new contract, either from HTML content or from your own PDF. The two forms differ in where the signature fields end up.

Request Body: HTML content

Signature fields are appended as a signature block below the content, in the order given. Coordinates are not accepted here: HTML flows, so there is no fixed page position to place a field at.

{
  "title": "My Contract",
  "content": "<p>Contract content in HTML...</p>",
  "language": "en",
  "signatureFields": [
    {
      "label": "Client"
    },
    {
      "label": "Provider"
    }
  ],
  "signaturesCentered": false,
  "tags": [
    "business"
  ],
  "finalize": true
}
  • language - Language of the signing page, the e-mails and the audit certificate: de, en, es, fr, it, pt, nl. Default en.
  • finalize - true creates a signable contract and returns its signUrl. false (the default) creates a draft that costs nothing until you finalize it with PATCH /contracts/:id.
  • signers: optional. A signer with an email gets a link of their own, sent to that address only: { label: "Client", email: "client@example.com" }, where label names a signature field label. Over that link only the fields of this label can be signed, and the certificate records the address as link delivered to, apart from an address a signer merely types. The shared signUrl keeps working for everyone else, so a contract can mix both: one party by delivered link, the other in the room. Links go out at once on a finalized contract, and for a draft or an unpaid contract as soon as it becomes signable (PATCH finalize, credit or payment). The response lists each link's delivery status in signers, never the link itself. A label that is no field label is refused with 400 and code: "UNKNOWN_ROLE" before anything is created. At most five sends per signer in 24 hours, after that 429 with code: "RATE_LIMITED".

Request Body: your own PDF

Pass the PDF as base64 in document.pdf (raw or as a data:application/pdf;base64, URL, up to 3 MB) and place each field with coordinates. At least one field is required: a PDF without one is a page nobody can sign.

{
  "title": "Order Confirmation 4711",
  "document": {
    "name": "order-4711.pdf",
    "pdf": "JVBERi0xLjQK..."
  },
  "signatureFields": [
    {
      "label": "Customer",
      "page": 2,
      "x": 10,
      "y": 78,
      "width": 25,
      "height": 10
    },
    {
      "label": "Sunrise Sales",
      "page": 2,
      "x": 60,
      "y": 78
    }
  ],
  "finalize": true
}

Coordinate system

  • page - 1-based page number of the PDF (default 1). Must not exceed the page count.
  • x, y - Top-left corner of the field box, in percent of the page width and height. Origin is the top-left corner of the page, so y: 0 is the top edge and y: 90 is near the bottom. Percentages make the placement independent of the page size (A4, Letter).
  • width, height - Box size in percent of the page (default 25 x 10). The whole box must lie within the page.
  • Fields with the same label belong to the same signer: one drawing fills all of them, for example initials on every page.

A field with coordinates but no document, or a box outside the page, is rejected with 400 and a list of the offending fields in details.

Response

{
  "success": true,
  "contract": {
    "id": "clx123...",
    "token": "ABC123XY",
    "title": "My Contract",
    "status": "pending",
    "language": "en",
    "signUrl": "https://canusign.com/sign/ABC123XY",
    "requiresPayment": false,
    "signers": [
      {
        "role": "Client",
        "email": "client@example.com",
        "status": "sent",
        "sentAt": "2024-01-15T10:30:01Z",
        "sendCount": 1,
        "openedAt": null,
        "signedAt": null
      }
    ],
    "createdAt": "2024-01-15T10:30:00Z"
  }
}
GET/contracts

List all your contracts.

Query Parameters

  • status - Filter by status: draft, pending, pending_payment, fully_signed. Anything else is a 400.
  • limit - Results per page (default: 50, max: 100)
  • offset - Pagination offset

Response

{
  "contracts": [
    {
      "id": "clx123...",
      "token": "ABC123XY",
      "title": "My Contract",
      "status": "pending",
      "signatures": [],
      "signers": [],
      "createdAt": "2024-01-15T10:30:00Z"
    }
  ],
  "pagination": {
    "total": 1,
    "limit": 50,
    "offset": 0,
    "hasMore": false
  }
}
GET/contracts/:id

Get contract details by ID or token. Includes statusUrl, the signatures so far, and pdfUrl, which is null until the contract is fully signed. Each signature says whether it came in over a delivered link (deliveredLink), and signers lists the delivered links with their status: created, sent, opened or signed.

publicId is the document ID printed on every page of the signed PDF, and verifyUrl the public page where anyone can compare a PDF with the stored copy (null for drafts). finalPdf.sha256 is the fingerprint of the stored PDF with its DigiCert timestamp: every download returns exactly these bytes. It is null until the PDF was built once.

GET/contracts/:id/pdf

Download the signed PDF: the contract or your document with all signatures embedded, any attachments, and the audit certificate. Authenticated with your API key like every other endpoint; the contract must belong to the key's account.

  • 200 - application/pdf, sent as an attachment
  • 409 - Not fully signed yet. The body carries code: "NOT_SIGNED" and the current status.
  • 404 - Unknown ID, or the contract belongs to another account

The usual flow: subscribe a webhook to contract.completed, then fetch the pdfUrl from its payload. Polling GET /contracts/:id until pdfUrl is set works too.

PATCH/contracts/:id

Rename or retag a contract, or finalize a draft. All fields are optional, at least one is required.

{
  "title": "Order Confirmation 4711",
  "tags": [
    "orders"
  ],
  "finalize": true
}
  • finalize: true - Turns a draft into a signable contract, subject to the same plan rules as creating it finalized. The response carries the signUrl. On anything but a draft you get 409 with code: "NOT_A_DRAFT".
  • title and tags can be changed until the contract is fully signed; afterwards 409 with code: "CONTRACT_SIGNED".
  • signers: same shape as on create. Adds or changes delivered links per label; a new address replaces the old link. A role that already signed over its link answers 409 with code: "ALREADY_SIGNED".
DELETE/contracts/:id

Delete a contract nobody has signed yet. As soon as one signature exists the contract is a record and stays: 409 with code: "CONTRACT_SIGNED".

GET/contracts/from-template

List all available templates with their variables. Use this to discover which templates and variable fields are available before creating a contract. signerRoles lists the labels of the signature fields, which signers[].label may name.

Query Parameters

  • language - Template language (de, en, es, fr, it, pt, nl). Default: en

Response

{
  "templates": [
    {
      "id": "dpa",
      "name": "Data Processing Agreement (EN)",
      "category": "legal",
      "icon": "🔐",
      "availableLanguages": [
        "de",
        "en",
        "es",
        "..."
      ],
      "variables": [
        {
          "id": "controller_name",
          "label": "Controller Name"
        },
        {
          "id": "processor_name",
          "label": "Processor Name"
        }
      ],
      "signerRoles": [
        "Controller",
        "Processor"
      ],
      "hasContent": true
    }
  ],
  "language": "en",
  "total": 45
}
POST/contracts/from-template

Create a contract from a pre-built template. No need to provide HTML — just choose a template, language, and fill in the variables.

Request Body

{
  "template": "dpa",
  "language": "en",
  "variables": {
    "controller_name": "Acme Corp",
    "processor_name": "Cloud Services GmbH",
    "controller_address": "123 Main St, Berlin",
    "processor_address": "456 Tech Ave, Munich"
  },
  "title": "DPA - Acme & Cloud Services",
  "tags": [
    "dpa",
    "gdpr"
  ],
  "signers": [
    {
      "label": "Processor",
      "email": "legal@cloud-services.example"
    }
  ],
  "finalize": true
}

signers works as on POST /contracts: each signer with an email gets a link of their own. The label must be one of the template's signerRoles, otherwise the request is refused with 400 before anything is created.

Response

{
  "success": true,
  "contract": {
    "id": "clx456...",
    "token": "DEF789AB",
    "title": "DPA - Acme & Cloud Services",
    "status": "pending",
    "signUrl": "https://canusign.com/sign/DEF789AB",
    "signers": [
      {
        "role": "Processor",
        "email": "legal@cloud-services.example",
        "status": "sent"
      }
    ],
    "createdAt": "2025-01-15T10:30:00Z"
  },
  "template": {
    "id": "dpa",
    "language": "en",
    "variablesFilled": 4,
    "variablesAvailable": 12
  }
}

Webhooks

Overview

Webhooks allow you to receive real-time HTTP notifications when events happen on your contracts. Instead of polling the API, register a URL and we'll send a POST request with event data whenever something changes.

Available Events

  • contract.created — A new contract was created
  • contract.signed — A signer submitted their signature
  • contract.completed — All required signatures collected
  • contract.deleted — A contract was deleted
  • signer.invited: a signing link went out to one signer's address (role, email, sentAt)

Payload Format

{
  "id": "del_abc123...",
  "event": "contract.signed",
  "created_at": "2025-01-15T10:30:00Z",
  "data": {
    "id": "clx123...",
    "token": "ABC123XY",
    "title": "Service Agreement",
    "fieldId": "sig-1",
    "signerName": "John Doe"
  }
}

contract.completed carries the download link instead of the signer:

{
  "id": "del_def456...",
  "event": "contract.completed",
  "created_at": "2025-01-15T11:02:00Z",
  "data": {
    "id": "clx123...",
    "token": "ABC123XY",
    "title": "Service Agreement",
    "signatureCount": 2,
    "pdfUrl": "https://canusign.com/api/v1/contracts/clx123.../pdf",
    "publicId": "7E2BBDC643834517",
    "verifyUrl": "https://canusign.com/verify/7E2BBDC643834517"
  }
}

Delivery

Your endpoint has five seconds to answer with a 2xx. Anything else counts as a failure, and we try again: once after three seconds, then after 10 minutes, 30 minutes, 2 hours, 6 hours and 24 hours. Seven attempts over roughly 33 hours, so a bad deploy on your side does not cost you the event. After the last one we stop, and you can pick the state up from GET /contracts/:id.

Every attempt of one event carries the same X-CanUSign-Delivery id and a fresh signature. De-duplicate on that id and treat your handler as repeatable. The URL must be public HTTPS; loopback and private addresses are refused. Recent deliveries with status code, attempt and duration are listed under GET /webhooks/:id.

HTTP Headers

  • X-CanUSign-Signature-V2 - the signature to verify: t=<unix seconds>,v1=<hex>. The timestamp is part of what is signed, so a captured delivery cannot be replayed later.
  • X-CanUSign-Signature - the older scheme over the body alone, sha256=<hex>. Still sent for receivers built before 07.09.2026, but it cannot tell a replay from a fresh delivery. Prefer V2.
  • X-CanUSign-Attempt - 1 for the first try, up to 7
  • X-CanUSign-Event — Event type (e.g. contract.signed)
  • X-CanUSign-Delivery — Unique delivery ID
  • User-Agent CanUSign-Webhook/1.0

Signature Verification

Every webhook delivery is signed with your webhook secret using HMAC-SHA256. Always verify the signature before processing events.

Sign the raw body, byte for byte

The signature covers the exact bytes we sent. Read the body as text before any JSON parsing, and never rebuild it: parsing and re-serializing changes the bytes, and the signature stops matching. A single non-ASCII character in a title is enough: Python's json.dumps turns an em dash into \u2014, while we send it as UTF-8. In Next.js use await req.text(), in Express express.raw(), in Flask request.get_data(). Parse the JSON only after the signature has checked out.

Node.js

const crypto = require('crypto');

// payload = the raw request body as a string, e.g. await req.text()
// header  = the X-CanUSign-Signature-V2 header, "t=<seconds>,v1=<hex>"
function verifyWebhook(payload, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.trim().split('='))
  );
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!Number.isFinite(age) || age > toleranceSeconds) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(parts.t + '.' + payload)
    .digest('hex');
  const a = Buffer.from(parts.v1 ?? '', 'hex');
  const b = Buffer.from(expected, 'hex');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Python

import hmac, hashlib, time

# payload = the raw request body as bytes, e.g. request.get_data()
# header  = the X-CanUSign-Signature-V2 header, "t=<seconds>,v1=<hex>"
def verify_webhook(payload: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.strip().split('=', 1) for p in header.split(','))
    try:
        age = abs(time.time() - int(parts['t']))
    except (KeyError, ValueError):
        return False
    if age > tolerance:
        return False
    signed = parts['t'].encode() + b'.' + payload
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(parts.get('v1', ''), expected)

Webhook Endpoints

GET/webhooks

List all your webhooks.

POST/webhooks

Create a new webhook endpoint, up to five per account. Returns 201.

Request Body

{
  "url": "https://your-server.com/webhooks",
  "events": [
    "contract.signed",
    "contract.completed"
  ]
}

Response

{
  "success": true,
  "webhook": {
    "id": "clx789...",
    "url": "https://your-server.com/webhooks",
    "events": [
      "contract.signed",
      "contract.completed"
    ],
    "active": true,
    "secret": "whsec_a1b2c3d4...",
    "createdAt": "2025-01-15T10:30:00Z"
  }
}

The secret is only returned on creation. Store it securely to verify webhook signatures.

PATCH/webhooks/:id

Update webhook URL, events, or active status. All fields are optional.

{
  "url": "https://new-server.com/hooks",
  "events": [
    "contract.completed"
  ],
  "active": false
}
DELETE/webhooks/:id

Delete a webhook.

GET/webhooks/:id

Get webhook details plus its last 20 deliveries (event, status code, attempt, duration), the place to look when an event did not arrive.

Examples (cURL)

Create contract with custom HTML

curl -X POST https://canusign.com/api/v1/contracts \
  -H "Authorization: Bearer canu_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Service Agreement",
    "content": "<h1>Service Agreement</h1><p>Terms...</p>",
    "finalize": true
  }'

Create contract from your own PDF with placed fields

PDF_B64=$(base64 < order-4711.pdf | tr -d '\n')
curl -X POST https://canusign.com/api/v1/contracts \
  -H "Authorization: Bearer canu_your_api_key" \
  -H "Content-Type: application/json" \
  -d "{
    \"title\": \"Order Confirmation 4711\",
    \"document\": { \"name\": \"order-4711.pdf\", \"pdf\": \"$PDF_B64\" },
    \"signatureFields\": [
      { \"label\": \"Customer\", \"page\": 2, \"x\": 10, \"y\": 78 },
      { \"label\": \"Vendor\", \"page\": 2, \"x\": 60, \"y\": 78 }
    ],
    \"finalize\": true
  }"

Download the signed PDF

curl -o signed.pdf https://canusign.com/api/v1/contracts/clx123.../pdf \
  -H "Authorization: Bearer canu_your_api_key"

Create contract from template

curl -X POST https://canusign.com/api/v1/contracts/from-template \
  -H "Authorization: Bearer canu_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "dpa",
    "language": "en",
    "variables": {
      "controller_name": "Acme Corp",
      "processor_name": "Cloud Services GmbH"
    },
    "title": "DPA - Acme & Cloud",
    "finalize": true
  }'

List available templates

curl https://canusign.com/api/v1/contracts/from-template?language=en \
  -H "Authorization: Bearer canu_your_api_key"

Claude Code / MCP Integration

Use CanUSign directly from Claude Code with our MCP server. Create contracts just by describing them in natural language.

1. Install the MCP Server

npx canusign-mcp-server

2. Configure Claude Code

Add to your ~/.claude/claude_desktop_config.json:

{
  "mcpServers": {
    "canusign": {
      "command": "npx",
      "args": [
        "canusign-mcp-server"
      ],
      "env": {
        "CANUSIGN_API_KEY": "canu_your_api_key_here"
      }
    }
  }
}

3. Use it!

Ask Claude to create contracts for you:

“Create a service agreement between Acme Corp and John Doe for web development at $5000”

“Create a DPA (Data Processing Agreement) between Acme Corp as controller and Cloud GmbH as processor for customer data processing”

“Send the signing link for the Client to anna@acme.example, I will sign as Provider on my laptop”

The MCP server uses the /contracts/from-template endpoint to create contracts from pre-built templates with pre-filled variables. Its tools create_contract, create_contract_from_template and update_contract take signers to e-mail links to individual signers.

Questions? Contact support