MCP Server

APISign provides a Model Context Protocol (MCP) server that enables AI agents to manage templates and send contracts programmatically.


Overview

The MCP server uses the Streamable HTTP transport and accepts two kinds of credentials:

  • OAuth. Paste https://apisign.io/mcp into Claude, ChatGPT, Cursor or any client that supports OAuth for remote servers. It sends you to APISign to sign in, choose an organization and approve, and connects. No API key to create or paste.
  • An API key in the x-api-key header, for clients you configure by hand, scripts, and anything running without a person to click through a sign-in.

Connection Details

PropertyValue
Endpointhttps://apisign.io/mcp
TransportStreamable HTTP, with sessions (mcp-session-id header). JSON-RPC goes over POST; GET opens the optional SSE stream; DELETE ends the session.
AuthenticationOAuth 2.1 (Authorization: Bearer with the access token), or an API key in the x-api-key header. An API key also works as Authorization: Bearer ask_…. Never in the query string.
Registry nameio.apisign/apisign

A request without valid credentials gets HTTP 401 with a JSON error and a WWW-Authenticate header pointing at the OAuth discovery document, which is how OAuth clients know to start signing you in.

The server publishes its own card at /.well-known/mcp.json, so tools that discover MCP servers automatically can find it without an API key.


Connecting with OAuth

Claude

In Claude (web, Desktop or mobile), open Settings > Connectors, choose Add custom connector, and enter https://apisign.io/mcp. Leave the OAuth client ID and secret empty. Claude opens APISign, where you:

  1. Sign in, or create an account.
  2. Choose the organization to connect, if you belong to more than one.
  3. Approve. Untick Create and send contracts to connect read-only.

Claude Code does the same from the terminal:

claude mcp add --transport http apisign https://apisign.io/mcp

then run /mcp inside Claude Code and choose Authenticate.

ChatGPT, Cursor and others

Any client that supports OAuth for remote MCP servers connects with just the URL. In Cursor, add the server with no headers:

{
  "mcpServers": {
    "apisign": {
      "url": "https://apisign.io/mcp"
    }
  }
}

What a connection can do

A connection acts for you in one organization. Approving with Create and send contracts ticked gives it all 12 tools; unticking it gives the four lookup tools, the same as a Read Only API key. Contracts it sends are billed to that organization like any other.

Every connected app is listed under Account > MCP & API Keys > Connected apps, with who connected it and when it was last used. Disconnect cuts it off on its next request. Leaving an organization disconnects your apps from it the same way.

For client developers

DocumentURL
Protected resource metadata (RFC 9728)/.well-known/oauth-protected-resource/mcp
Authorization server metadata (RFC 8414)/.well-known/oauth-authorization-server/api/auth

Clients register themselves with dynamic client registration (RFC 7591) and are public clients: authorization code with PKCE (S256), no client secret. Scopes are read, write and offline_access. Access tokens last an hour; refresh tokens rotate on every use, and replaying a used one revokes the whole grant. Loopback redirect URIs (localhost and 127.0.0.1) match on any port.


Getting an API Key

  1. Log into the APISign dashboard
  2. Navigate to Account > MCP & API Keys
  3. Click Create API Key
  4. Copy the key (it won't be shown again)

Key permissions

The permission you pick when creating the key decides which tools it can use.

PermissionTools available
Read Only (MCP tools)template_list, template_get, contract_list, contract_get
Read/WriteAll 12 tools

A Read Only key does not see the write tools at all — they are not registered for its session, so they are missing from tools/list. Calling one by name anyway returns a tool error reading MCP error -32602: Tool contract_send not found.

If your agent only ever needs to look things up, use a Read Only key. Over MCP it cannot create, send, cancel, or archive anything.

The permission only applies to MCP

The REST API does not check a key's permission. A Read Only key sent to /api/* — directly or through the CLI — can do everything a Read/Write key can.


Configuring a client with an API key

Claude Code

claude mcp add --transport http apisign https://apisign.io/mcp --header "x-api-key: your-api-key-here"

Cursor

Add this to ~/.cursor/mcp.json (%USERPROFILE%\.cursor\mcp.json on Windows), or to .cursor/mcp.json in a project:

{
  "mcpServers": {
    "apisign": {
      "url": "https://apisign.io/mcp",
      "headers": {
        "x-api-key": "your-api-key-here"
      }
    }
  }
}

Claude Desktop

Use OAuth (above) unless you specifically need an API key. To use one, claude_desktop_config.json only launches local (stdio) servers, so run mcp-remote as a local bridge (requires Node.js):

{
  "mcpServers": {
    "apisign": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://apisign.io/mcp",
        "--header",
        "x-api-key:${APISIGN_API_KEY}"
      ],
      "env": {
        "APISIGN_API_KEY": "your-api-key-here"
      }
    }
  }
}

The file is at ~/Library/Application Support/Claude/claude_desktop_config.json on macOS and %APPDATA%\Claude\claude_desktop_config.json on Windows. Restart Claude Desktop after editing it.

Other clients

add-mcp writes the config for the coding agents it detects — Cursor, Claude Code, VS Code, Codex, and others:

npx add-mcp https://apisign.io/mcp --header "x-api-key: your-api-key-here"

The header must be in Name: value form. add-mcp does not configure remote servers for Claude Desktop; use the bridge above.

Local Development

Running APISign itself locally (bun dev), the server listens on port 3555:

claude mcp add --transport http apisign-local http://localhost:3555/mcp --header "x-api-key: your-api-key-here"

Available Tools

Template Tools

template_list

List all non-archived templates in your organization, newest first.

Parameters: None

Returns:

{
  "templates": [
    {
      "id": "clx...",
      "name": "NDA Template",
      "created_at": "2024-01-15T10:30:00Z",
      "updated_at": "2024-01-15T10:30:00Z"
    }
  ]
}

template_get

Get a specific template with its content and field definitions. Archived templates return Template not found.

Parameters:

NameTypeRequiredDescription
idstringYesTemplate ID (CUID format)

Returns the full template record (abbreviated here). fields is null until field definitions are set.

{
  "template": {
    "id": "clx...",
    "name": "NDA Template",
    "content": "# Non-Disclosure Agreement\n\nThis agreement...",
    "fields": {
      "fields": [
        { "name": "company_name", "type": "text", "completedBy": "creator" },
        { "name": "signature", "type": "signature", "completedBy": "signer" }
      ]
    }
  }
}

template_create

Create a new template, blank or with content.

Parameters:

NameTypeRequiredDescription
namestringYesTemplate name (min 3 characters)
contentstringNoMarkdown content. If given, it must contain at least one signature placeholder such as {{signature}} — see Template Variables.

Example:

{
  "name": "Service Agreement",
  "content": "# Service Agreement\n\nBetween {{company_name}} and {{client_name}}...\n\n{{signature}}"
}

Returns { "success": true, "template": { ... } }.


template_update

Update an existing template.

Parameters:

NameTypeRequiredDescription
idstringYesTemplate ID
namestringNoNew name (min 3 characters)
contentstringNoNew markdown content
fieldsarrayNoField definitions. Replaces the whole list. Each entry needs name, type, and completedBy (creator or signer).

If the template has any content after the update — new or existing content, checked against new or existing fields — it must still contain a signature field completed by the signer, or the update is refused.

Returns { "success": true, "template": { ... } }.

Fields array format:

{
  "fields": [
    {
      "name": "company_name",
      "type": "text",
      "completedBy": "creator"
    },
    {
      "name": "signature",
      "type": "signature",
      "completedBy": "signer"
    }
  ]
}

template_upload

Upload a Word .docx file and convert it to a template. Legacy .doc files aren't supported; save them as .docx first.

Parameters:

NameTypeRequiredDescription
filenamestringYesOriginal filename (must end in .docx). The template is named after it.
fileDatastringYesBase64-encoded file content

Returns { "success": true, "template": { ... }, "warnings": [ ... ] }, where warnings lists anything the conversion could not carry over. The new template has no field definitions yet; set them with template_update.


template_archive

Archive (soft delete) a template. Archived templates disappear from template_list and can no longer be used by contract_create.

Parameters:

NameTypeRequiredDescription
idstringYesTemplate ID

Returns { "success": true, "message": "Template archived successfully" }.


Contract Tools

contract_list

List every contract in your organization, newest first. There is no pagination.

Parameters:

NameTypeRequiredDescription
statusstringNoFilter by status: draft, queued, sent, signed, cancelled, expired. A fully signed contract is signed; there is no completed status.

Returns:

{
  "contracts": [
    {
      "id": "clx...",
      "name": "NDA - Acme Corp",
      "status": "sent",
      "created_at": "2024-01-15T10:30:00Z",
      "updated_at": "2024-01-15T11:00:00Z",
      "sent_at": "2024-01-15T11:00:00Z",
      "expires_at": "2024-02-14T11:00:00Z"
    }
  ]
}

contract_get

Get a contract with its signers, ordered by signing_order. Each signer's email_delivery says whether their latest signing request was delivered (null if they haven't been emailed). The statuses are listed under Email Delivery.

Parameters:

NameTypeRequiredDescription
idstringYesContract ID (CUID format)

Returns the full contract record and signer records (abbreviated here). template is null for contracts created from custom content.

{
  "contract": {
    "id": "clx...",
    "name": "NDA - Acme Corp",
    "status": "sent",
    "test_mode": false,
    "expires_at": "2024-02-14T11:00:00Z",
    "content": { "markdown": "..." },
    "template": { "id": "clx...", "name": "NDA Template" }
  },
  "signers": [
    {
      "id": "clx...",
      "name": "John Doe",
      "email": "john@example.com",
      "status": "pending",
      "signing_order": 1,
      "email_delivery": {
        "status": "delivered",
        "recipient": "john@example.com",
        "sent_at": "2024-01-15T11:00:00Z",
        "status_updated_at": "2024-01-15T11:00:03Z",
        "detail": null
      }
    }
  ]
}

contract_create

Create a new contract, in draft status, from a template or custom content. Creating is free; contract_send is what charges.

Parameters:

NameTypeRequiredDescription
namestringYesContract name (1–255 characters)
template_idstringNoTemplate ID (if using a template)
contentstringNoMarkdown content. Required if there is no template_id; ignored if there is.
variablesobjectNoString values for the template's variables. Every field the template marks completedBy: "creator" must be given a non-empty value.
expires_in_daysintegerNoDays until the signing links stop working (default: 30, min: 1, max: 365). Cannot be changed later.
test_modebooleanNoCreate as a test contract (default: false). See below.
signersarrayYesList of signers (at least one)

The rendered content must contain at least one signature field. With more than one signer, every signer needs their own signature field, assigned through signer_fields.

Test mode

test_mode: true marks the contract as a rehearsal:

  • contract_send never charges it and needs no balance (it still needs a billing setup),
  • the signing emails go to your organization's billing email (or its owner) instead of the signers, marked [TEST], and so does the completed PDF,
  • the document is stamped TEST and is not digitally sealed.

It can only be set here, at creation. contract_update has no test_mode parameter, so a contract cannot switch between test and live partway through.

Signers array format:

FieldTypeRequiredDescription
emailstringYesSigner's email address
namestringYesSigner's full name (1–255 characters)
signing_orderintegerYes1-based and unique across the contract's signers. Display order only — every signer is emailed at send and may sign in any order.
companystringNoCompany name
mobilestringNoMobile number
signer_fieldsstring[]NoVariable names this signer fills in, without braces. Required for every signer when there is more than one.
{
  "signers": [
    {
      "email": "john@example.com",
      "name": "John Doe",
      "company": "Acme Corp",
      "signing_order": 1,
      "signer_fields": ["signature_john", "date_john"]
    },
    {
      "email": "jane@example.com",
      "name": "Jane Smith",
      "signing_order": 2,
      "signer_fields": ["signature_jane", "date_jane"]
    }
  ]
}

Example - From template:

{
  "name": "NDA - Acme Corp",
  "template_id": "clx...",
  "variables": {
    "company_name": "Acme Corporation",
    "effective_date": "January 15, 2024"
  },
  "signers": [
    {
      "email": "john@acme.com",
      "name": "John Doe",
      "signing_order": 1
    }
  ]
}

Example - Custom content:

{
  "name": "Simple Agreement",
  "content": "# Agreement\n\nI, {{signer_name}}, agree to the terms.\n\n{{signature}}",
  "signers": [
    {
      "email": "john@example.com",
      "name": "John Doe",
      "signing_order": 1
    }
  ]
}

Returns { "success": true, "contract": { ... }, "signers": [ ... ] }. Unlike the REST /contract/create route, this tool does not emit a contract_created webhook event.


contract_update

Update a draft contract. Only contracts with draft status can be updated.

Parameters:

NameTypeRequiredDescription
contract_idstringYesContract ID
namestringNoNew name
contentobjectNoReplaces the content object, e.g. { "markdown": "..." }
signersarrayNoReplaces the whole signer list. Same fields as in contract_create, except signer_fields: assignments made at creation stay attached to each signing_order.

The contract as it will be after the update must still give every signer a signature field. Returns { "success": true, "contract": { ... }, "signers": [ ... ] }.


contract_send

Send a draft contract to its signers, or resend a sent one. This:

  • Changes status to sent and sets sent_at
  • Emails a signing link to every signer who hasn't signed yet. On a resend, signers who already signed keep their signature and aren't emailed.
  • Charges your organization balance the per-contract price ($0.25 unless your organization has a different rate) on every send and every resend, unless the contract is in test mode

It requires a billing setup, and a balance of at least the price for live contracts. It refuses a contract whose signing deadline has passed, and a resend where every signer has already signed; neither is charged.

Parameters:

NameTypeRequiredDescription
contract_idstringYesContract ID

Returns:

{
  "success": true,
  "contract_id": "clx...",
  "signers_notified": 2,
  "cost_charged": 0.25,
  "message": "Contract sent successfully"
}

cost_charged is in dollars and is 0 for a test contract. A resend returns "Contract resent successfully".


contract_cancel

Cancel a contract. Only draft and sent contracts can be cancelled. Signing links stop working; nothing is refunded.

Parameters:

NameTypeRequiredDescription
contract_idstringYesContract ID

Returns { "success": true, "message": "Contract cancelled successfully" }.


Template Variables

Templates use {{variable_name}} placeholders. A variable is filled in by the creator (through contract_create's variables) only if the template's fields marks it completedBy: "creator"; everything else is left for the signer. A variable with no type in fields gets one from the words in its name, so {{tenant_signature}} becomes a signature, {{tenant_sign_date}} a timestamp and {{tenant_signer_name}} the signer's name. The full rules are in the Templates API.


Error Handling

Errors arrive in one of three shapes:

Where it failsWhat you get
AuthenticationHTTP 401 with a JSON body, e.g. {"error": "Invalid or disabled API key"}, and a WWW-Authenticate header. No MCP session is created.
Arguments don't match the tool's schemaA tool result with isError: true and plain text: MCP error -32602: Input validation error: Invalid arguments for tool <name>: [...], followed by the validation issues.
Everything elseA tool result with isError: true whose text is a JSON string: {"error": "..."}.

Common Errors

ErrorDescription
Missing credentials: send an x-api-key header or sign in with OAuthNo API key or access token (HTTP 401)
Invalid access token / Access token has expiredOAuth token unknown, revoked or expired; the client refreshes it or signs in again (HTTP 401)
You are no longer a member of this organizationThe person who connected the app left the organization (HTTP 401)
Invalid or disabled API keyAPI key not found or disabled (HTTP 401)
API key has expiredAPI key past expiration date (HTTP 401)
MCP error -32602: Tool <name> not foundRead Only key called a write tool, or the name is wrong
Template not foundTemplate ID doesn't exist, belongs to another organization, or is archived
Contract not foundContract ID doesn't exist or belongs to another organization
Either content or template_id must be providedcontract_create with neither
Missing required variables: <names>. ...contract_create without a value for every creator field
Signing orders must be uniqueTwo signers share a signing_order
Document must contain at least one signature field. ...Content has no signature placeholder completed by the signer
Only draft contracts can be updatedcontract_update on a contract that isn't draft
No billing setup foundcontract_send before the organization has billing set up
Insufficient balance. Need $0.25 to send.Balance is below the per-contract price (the amount shown is your organization's price)
Contract has no signerscontract_send on a contract with no signers
Cannot send contract with status '<status>'contract_send on anything but draft or sent
Cannot cancel contract with status '<status>'contract_cancel on anything but draft or sent

Usage Examples

Create and Send a Contract

1. Use template_list to find available templates
2. Use template_get to view template content and fields
3. Use contract_create with template_id and variables
4. Use contract_send to send to signers

Upload a Document Template

1. Read document file and base64 encode it
2. Use template_upload with filename and fileData
3. Use template_update to set field definitions

Check Contract Status

1. Use contract_list with status filter
2. Use contract_get for detailed signer status

Rate Limits

We do not currently enforce per-API-key rate limits. There is no published quota to plan against and no RateLimit-* headers on API responses.

The only rate-limited endpoint is the unauthenticated support contact form. Neither /mcp nor any authenticated API route is.

Planning for high volume?

If we add per-key limits, they will be documented here with their numbers before they are enforced. Until then, be a good citizen and retry with backoff.