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/mcpinto 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-keyheader, for clients you configure by hand, scripts, and anything running without a person to click through a sign-in.
Connection Details
| Property | Value |
|---|---|
| Endpoint | https://apisign.io/mcp |
| Transport | Streamable HTTP, with sessions (mcp-session-id header). JSON-RPC goes over POST; GET opens the optional SSE stream; DELETE ends the session. |
| Authentication | OAuth 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 name | io.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:
- Sign in, or create an account.
- Choose the organization to connect, if you belong to more than one.
- 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
| Document | URL |
|---|---|
| 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
- Log into the APISign dashboard
- Navigate to Account > MCP & API Keys
- Click Create API Key
- 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.
| Permission | Tools available |
|---|---|
| Read Only (MCP tools) | template_list, template_get, contract_list, contract_get |
| Read/Write | All 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:
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Template 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:
| Name | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Template name (min 3 characters) |
content | string | No | Markdown 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:
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Template ID |
name | string | No | New name (min 3 characters) |
content | string | No | New markdown content |
fields | array | No | Field 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:
| Name | Type | Required | Description |
|---|---|---|---|
filename | string | Yes | Original filename (must end in .docx). The template is named after it. |
fileData | string | Yes | Base64-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:
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Template 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:
| Name | Type | Required | Description |
|---|---|---|---|
status | string | No | Filter 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:
| Name | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Contract 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:
| Name | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Contract name (1–255 characters) |
template_id | string | No | Template ID (if using a template) |
content | string | No | Markdown content. Required if there is no template_id; ignored if there is. |
variables | object | No | String values for the template's variables. Every field the template marks completedBy: "creator" must be given a non-empty value. |
expires_in_days | integer | No | Days until the signing links stop working (default: 30, min: 1, max: 365). Cannot be changed later. |
test_mode | boolean | No | Create as a test contract (default: false). See below. |
signers | array | Yes | List 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_sendnever 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
TESTand 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:
| 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 | 1-based and unique across the contract's signers. Display order only — every signer is emailed at send and may sign in any order. |
company | string | No | Company name |
mobile | string | No | Mobile number |
signer_fields | string[] | No | Variable 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:
| Name | Type | Required | Description |
|---|---|---|---|
contract_id | string | Yes | Contract ID |
name | string | No | New name |
content | object | No | Replaces the content object, e.g. { "markdown": "..." } |
signers | array | No | Replaces 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
sentand setssent_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:
| Name | Type | Required | Description |
|---|---|---|---|
contract_id | string | Yes | Contract 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:
| Name | Type | Required | Description |
|---|---|---|---|
contract_id | string | Yes | Contract 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 fails | What you get |
|---|---|
| Authentication | HTTP 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 schema | A 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 else | A tool result with isError: true whose text is a JSON string: {"error": "..."}. |
Common Errors
| Error | Description |
|---|---|
Missing credentials: send an x-api-key header or sign in with OAuth | No API key or access token (HTTP 401) |
Invalid access token / Access token has expired | OAuth token unknown, revoked or expired; the client refreshes it or signs in again (HTTP 401) |
You are no longer a member of this organization | The person who connected the app left the organization (HTTP 401) |
Invalid or disabled API key | API key not found or disabled (HTTP 401) |
API key has expired | API key past expiration date (HTTP 401) |
MCP error -32602: Tool <name> not found | Read Only key called a write tool, or the name is wrong |
Template not found | Template ID doesn't exist, belongs to another organization, or is archived |
Contract not found | Contract ID doesn't exist or belongs to another organization |
Either content or template_id must be provided | contract_create with neither |
Missing required variables: <names>. ... | contract_create without a value for every creator field |
Signing orders must be unique | Two 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 updated | contract_update on a contract that isn't draft |
No billing setup found | contract_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 signers | contract_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.