The manual

Docs.

Everything here speaks plain HTTP. Send a document with one request. No key needed: we email you a one-time code and hand back a throwaway key for that document. Log in to mint live keys. On-page fields use PDF tags or fields JSON; there is no placer and no sign tool.

View as machine

Send

POST /v1/documents
$ curl -F title='Repair authorization' \
       -F sender_email=you@example.com \
       -F signers='[{"name":"Jane",
         "email":"jane@example.com"}]' \
       -F file=@form.pdf \
       https://agentsign.co/v1/documents
{ "id": "doc_kx3q9", "status": "sent" }

Keys

sign_live_ sends, lists, and downloads. Mint one after you log in.

sign_tmp_ follows one document: status and download only. It cannot send. You get one when you send without logging in.

sign_agent_ names an agent. It attests on its turn and gets a cryptographic receipt, not a pretend signature. It never signs for a person.

View as machine

Status

GET /v1/documents/{id}
$ curl https://agentsign.co/v1/documents/doc_kx3q9 \
       -H 'authorization: Bearer sign_live_...'
{ "status": "completed", "signers": [ … ], "audit": [ … ] }

MCP

The same surface over MCP, at https://agentsign.co/mcp. Streamable HTTP, one endpoint, no gateway to install.

Hosts that speak OAuth only need that URL: we publish /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server, register the client at /oauth/register, and run OAuth 2.1 with PKCE (S256). An unauthenticated call answers 401 with that metadata, so the host finds the rest on its own. Hosts that take a secret instead can pass a key as authorization: Bearer sign_live_ and skip OAuth.

  • send creates and sends a document, markdown or PDF bytes.
  • status reports where a document stands, party by party.
  • download returns the sealed PDF.
  • attest takes an agent party’s turn.
  • reject declines it and stops the document.
  • verify checks a sealed file. No key on the REST endpoint.
  • list_templates lists your saved templates.
  • send_template sends one, signers in role order.

There is no sign tool, and there will not be one. A key says who is calling; it never stands in for a person. Humans finish, agents attest.

View as machine

MCP

POST /mcp
# Claude Code
$ claude mcp add --transport http agentsign \
       https://agentsign.co/mcp

# Cursor (mcp.json)
{ "mcpServers": { "agentsign": {
    "url": "https://agentsign.co/mcp" } } }

# Claude Desktop, claude.ai
Settings > Connectors > Add custom connector
URL: https://agentsign.co/mcp

# No MCP host? Speak it yourself.
$ curl -X POST https://agentsign.co/mcp \
       -H 'authorization: Bearer sign_live_...' \
       -H 'accept: application/json, text/event-stream' \
       -H 'content-type: application/json' \
       -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
         "params":{"protocolVersion":"2025-11-25",
         "capabilities":{},"clientInfo":
         {"name":"curl","version":"1"}}}'

Agents

An agent party is a party on the document with kind: agent and a slug you registered at /agents. It gets no signing link and no ceremony. When its turn comes it attests or rejects over the API, and the receipt names the agent and the person it acted for. An agent party carries the account owner’s email address; give it any other email and the send is rejected.

A sign_agent_ key attests and rejects, for its own agent and nothing else. A sign_live_ key, a session, or an OAuth grant attests by naming an agent the team owns. Sending needs a live key, a session, or an OAuth grant.

Each agent can carry a webhook URL. We POST party.ready, document.completed, document.declined, and document.expired to it, signed with X-Sign-Signature (sha256= HMAC of timestamp.body) and X-Sign-Timestamp. The secret is shown once, when you set the URL.

A send from an OAuth grant is held at pending_sender until you enter the code we email you. Turn that off under Settings, Security, Send confirmation. Live keys are standing authorizations and always send at once.

human_required
Every party attested and nobody signed. A document needs a human signer, so add one when sending.
invalid_state
The document is not awaiting attestation: already complete, declined, or expired.
cannot_attest
This caller may not attest as that agent, or it is not that agent's turn.
unknown_agent
A signer named an agent slug that does not exist on this team.
agent_limit
Ten agents per team.
pro_required
Agent parties need Pro. Self-host is entitled.
flag_off
Agent parties are switched off on this deployment.
invalid_request
An agent party's email didn't match the agent owner's account, or the request was otherwise malformed.
slug_taken
That agent slug is already registered on this team.
View as machine

Attest

POST /v1/documents/{id}/attest
$ curl -X POST \
       https://agentsign.co/v1/documents/doc_kx3q9/attest \
       -H 'authorization: Bearer sign_agent_...'
{ "status": "pending" }

# webhook POST to your agent URL
# X-Sign-Timestamp: 1756000000
# X-Sign-Signature: sha256=hmac(secret, "timestamp.body")
{ "event": "party.ready", "id": "doc_kx3q9",
  "agent": "claude-ops", "status": "pending" }

Verify

Anyone can POST a sealed file back and get the verdict: valid or not, plus how many people signed and how many agents attested. No key, no account. The file is the proof.

View as machine

Verify

POST /v1/verify
$ curl -F file=@sealed.pdf \
       https://agentsign.co/v1/verify
{ "valid": true, "human_signatures": 1, "agent_attestations": 1 }

Embed

Iframe the ceremony at /s/:token. Set embed_origin and listen for postMessage. Use send_email=false when you deliver the link yourself. Tags and fields JSON place signatures; we do not ship a placer.

View as machine

Embed

GET /s/{token}
<iframe src="https://agentsign.co/s/TOKEN"></iframe>
// listen for { source: "agentsign", event }
// optional: embed_origin, send_email=false, fields JSON, PDF {{sig}}
Docs · AgentSign