md2sign API & MCP reference
md2sign turns a markdown contract into a legally binding e-signature request. You write markdown with signature fields, we email each signer a unique link, they read the document, type their name, and sign. Every signature is hashed (SHA-256), hash-chained and sealed with Ed25519.
- MCP server (streamable HTTP):
https://md2sign.com/mcp - REST base URL:
https://md2sign.com/api/v1 - OpenAPI:
https://md2sign.com/openapi.json - LLM summary:
https://md2sign.com/llms.txt - Pricing: Starter $4.76 USD/month for 50 signature requests; Pro $26.41 USD/month for 500. One signature request = one recipient on one sent document. Validating, drafting, previewing, test sends and reminders are free.
1. The markdown format
Standard CommonMark plus GitHub tables, with two additions.
Signature fields
[[signature: First Last <[email protected]>]]
[[signature: First Last <[email protected]> | role: Director, Acme Pty Ltd]]
- One field per signer, ideally on its own line. Each unique email is one recipient.
- Repeat a field to place the same signer in several places; they sign once and every placement shows the signature.
- The same email must always use the same name. Use the signer's first and last name.
- Up to 25 recipients per document.
Hidden directives
HTML comments, so they stay invisible in any markdown viewer:
<!-- md2sign:style=modern --> classic (default) | modern | formal
<!-- md2sign:title=Consulting Agreement -->
Without a title directive, the first # Heading is the title.
Example
<!-- md2sign:style=formal -->
# Consulting Agreement
**Acme Pty Ltd** engages **Jane Citizen** to provide design services from 1 December 2026.
## Fees
| Item | Amount |
|------|--------|
| Monthly retainer | $4,000 |
## Signatures
[[signature: Alex Founder <[email protected]> | role: Director]]
[[signature: Jane Citizen <[email protected]>]]
2. Quick start for agents
- Validate (no account):
POST /api/v1/validate{ "markdown": "..." } - Preview (no account):
POST /api/v1/render{ "markdown": "...", "view": "signing_page" | "email", "format": "html" }. Show the HTML to your human. - Create an account for your human:
POST /api/v1/signup{ "name", "email", "company_name" }returnsapi_keyandcheckout_urls. The human gets a confirmation email. Give them acheckout_urlto subscribe. - Draft:
POST /api/v1/documents{ "markdown": "..." }withAuthorization: Bearer <api_key>. Update it withPATCH /api/v1/documents/{id}as often as you like. - Test send (free):
POST /api/v1/documents/{id}/test-sendemails a watermarked copy to the account owner. - Send:
POST /api/v1/documents/{id}/send{ "message": "optional note" }. Requires a confirmed email and an active plan. - Track:
GET /api/v1/documents/{id}andGET /api/v1/documents/{id}/audit.
curl -s https://md2sign.com/api/v1/validate \
-H "Content-Type: application/json" \
-d '{"markdown":"# NDA\n\nTerms...\n\n[[signature: Jane Citizen <[email protected]>]]"}'
3. MCP
Add the server to any MCP client that supports remote streamable HTTP servers:
{
"mcpServers": {
"md2sign": {
"type": "http",
"url": "https://md2sign.com/mcp",
"headers": { "Authorization": "Bearer YOUR_API_KEY" }
}
}
}
Claude Code: claude mcp add --transport http md2sign https://md2sign.com/mcp --header "Authorization: Bearer YOUR_API_KEY"
No key yet? Connect without the header and call create_account. Every authenticated tool also accepts an api_key argument for clients that can't set headers.
| Tool | Auth | What it does |
|---|---|---|
get_markdown_spec |
no | Format rules and an example contract |
validate_markdown |
no | Recipients, title, style, units, hash, errors with line numbers |
render_preview |
no | HTML of the signing page or the email |
create_account |
no | Creates the human's account; returns api_key and checkout_urls |
get_account |
yes | Plan, usage, verification, checkout links |
create_draft |
yes | Save a draft (send: true to send now) |
update_draft |
yes | Replace markdown/title/style/message of a draft |
get_document / list_documents |
yes | Status and per-recipient progress |
test_send_document |
yes | Free watermarked copy to the owner |
send_document |
yes | Send for signature |
remind_recipients |
yes | Re-email unsigned recipients |
void_document |
yes | Cancel a sent document |
get_audit_trail |
yes | Events, hashes, seals, verification |
list_templates |
yes (Pro) | Your company's shared templates |
get_template |
yes (Pro) | Latest markdown, {{ variables }}, version history; version for an older one |
save_template |
yes (Pro) | Create (no template_id) or update; markdown changes create a new version |
create_document_from_template |
yes (Pro) | Fill variables, create a draft, send: true to send now |
list_webhooks |
yes (Pro admin) | Webhook endpoints, their secrets and the available events |
save_webhook |
yes (Pro admin) | Create or update an endpoint; send_test: true sends a test event |
4. REST endpoints
All responses are JSON. Authenticated endpoints need Authorization: Bearer <api_key>. Send a descriptive User-Agent header, such as my-agent/1.0. Our edge network rejects Python's default Python-urllib/x.y user agent with HTTP 403 (Cloudflare error 1010); requests, httpx, Node, Go and curl defaults work.
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /api/v1/spec |
no | Markdown format as JSON |
| POST | /api/v1/validate |
no | markdown |
| POST | /api/v1/render |
no | markdown, view (signing_page, email, fragment), style, message, format (json, html) |
| POST | /api/v1/signup |
no | name, email, company_name (5 per hour per IP) |
| GET | /api/v1/me |
yes | Account, plan and usage |
| GET | /api/v1/billing/checkout |
yes | Checkout links to give the human |
| GET | /api/v1/documents |
yes | status, per_page |
| POST | /api/v1/documents |
yes | markdown, title, style, message, external_reference, send |
| GET | /api/v1/documents/{id} |
yes | |
| PATCH | /api/v1/documents/{id} |
yes | Drafts only |
| DELETE | /api/v1/documents/{id} |
yes | Drafts only |
| GET | /api/v1/documents/{id}/preview |
yes | view, format |
| POST | /api/v1/documents/{id}/test-send |
yes | Free, 10/day |
| POST | /api/v1/documents/{id}/send |
yes | message |
| POST | /api/v1/documents/{id}/remind |
yes | Once per hour per recipient |
| POST | /api/v1/documents/{id}/void |
yes | reason |
| GET | /api/v1/documents/{id}/audit |
yes | Events and cryptographic verification |
| GET | /api/v1/documents/{id}/pdf |
yes | Signed PDF with audit certificate |
| GET | /api/v1/templates |
yes | Templates shared by your company (Pro) |
| POST | /api/v1/templates |
yes | name, markdown, description, change_note (Pro) |
| GET | /api/v1/templates/{id} |
yes | Latest markdown, variables, versions |
| PATCH | /api/v1/templates/{id} |
yes | name, description, markdown, change_note; new markdown = new version |
| DELETE | /api/v1/templates/{id} |
yes | Creator or company admin |
| GET | /api/v1/templates/{id}/versions/{version} |
yes | One version's markdown |
| POST | /api/v1/templates/{id}/versions/{version}/restore |
yes | Copies an old version to a new latest version |
| POST | /api/v1/templates/{id}/documents |
yes | variables, version, title, style, message, send |
| GET | /api/v1/webhooks |
yes | Endpoints and available events (Pro admin) |
| POST | /api/v1/webhooks |
yes | url, events, include_pdf, description, is_active |
| GET | /api/v1/webhooks/{id} |
yes | Endpoint plus its 25 most recent deliveries |
| PATCH | /api/v1/webhooks/{id} |
yes | Any of the fields above |
| DELETE | /api/v1/webhooks/{id} |
yes | |
| POST | /api/v1/webhooks/{id}/test |
yes | Sends a webhook.test event |
| POST | /api/v1/webhooks/{id}/roll-secret |
yes | Issues a new signing secret |
Errors
{ "error": "subscription_required", "message": "An active subscription is required to send documents.", "details": { "checkout_urls": { "starter": "...", "pro": "..." } } }
| Code | HTTP | Meaning |
|---|---|---|
invalid_document |
422 | Markdown errors; see details.errors |
email_unverified |
403 | The human hasn't clicked the confirmation link |
subscription_required |
402 | No active plan; details.checkout_urls |
quota_exceeded |
402 | Not enough signature requests left this month |
not_a_draft |
409 | Sent documents can't be edited; void and resend |
account_exists |
409 | Ask the human for an API key from the dashboard |
pro_required |
402 | Templates, branding and team members need the Pro plan |
missing_variables |
422 | Template variables without values; see details.missing_variables |
rate_limited |
429 | Slow down |
5. Pro: company accounts, branding and templates
- Company accounts. Every account is a company account. On Pro, the admin can invite colleagues with the same email domain (personal providers such as gmail.com can't add members). Members share the plan's 500 monthly signature requests, templates and branding. Members see their own documents; the admin sees every document in the company and manages billing. The admin role can be handed to another member.
GET /api/v1/mereturnsteam(your role and the admin's email) andpro_features. - Branding. The admin uploads a logo on the Company page. It appears on signature emails, signing pages and signed PDFs.
- Webhooks. Post document events, the markdown and the signed PDF to your own systems. See Webhooks.
- Templates. Versioned markdown shared across the company. Use
{{ variable_name }}placeholders for anything that changes per document, including signers:[[signature: {{ client_name }} <{{ client_email }}>]]. Every markdown change creates a new version; older versions stay readable and can be restored. Documents record which template version they came from (document.template).
6. Webhooks
Pro company admins can add up to 5 webhook endpoints, in the dashboard (Developers → Webhooks), over the REST API or through MCP. md2sign POSTs JSON to each endpoint when one of its subscribed events happens to any company document. Point it at Zapier, Make, n8n, your CRM or your own server.
| Event | When |
|---|---|
document.sent |
A document is sent for signature |
document.signed |
One signer signs (recipient says who) |
document.completed |
Everyone has signed. Includes the signed PDF if include_pdf is on |
document.declined |
A signer declines |
document.voided |
The sender cancels |
document.expired |
The document expires unsigned |
webhook.test |
You pressed "Send test" |
Every payload includes the full markdown as text:
{
"id": "01JD…",
"event": "document.completed",
"created_at": "2026-11-12T09:41:00+00:00",
"document": {
"id": "01JC…", "title": "Consulting Agreement", "status": "completed",
"external_reference": "deal-4521",
"template": { "id": "01JB…", "version": 3 },
"document_hash": "9f2c…",
"created_by": { "name": "Jason Law", "email": "[email protected]" },
"recipients": [ { "name": "Jane Citizen", "email": "[email protected]", "status": "signed", "signed_at": "…" } ],
"seal": { "algorithm": "Ed25519", "hash": "…", "signature": "…" },
"links": { "verify": "https://md2sign.com/verify/01JC…", "pdf": "https://md2sign.com/api/v1/documents/01JC…/pdf" }
},
"markdown": "# Consulting Agreement\n\n…",
"pdf": { "filename": "consulting-agreement.pdf", "content_type": "application/pdf", "encoding": "base64", "data": "JVBERi0xLjcK…" }
}
Verify every delivery. Each request has Md2sign-Event, Md2sign-Delivery and Md2sign-Signature: t=<unix time>,v1=<hex> headers. v1 is the HMAC-SHA256 of "<t>.<raw request body>", keyed with the endpoint's signing secret. Compare it in constant time and reject timestamps older than 5 minutes.
[$t, $v1] = sscanf($request->header('Md2sign-Signature'), 't=%d,v1=%s');
$expected = hash_hmac('sha256', $t.'.'.$request->getContent(), $secret);
abort_unless(hash_equals($expected, (string) $v1) && abs(time() - $t) < 300, 401);
const [, t, v1] = req.headers['md2sign-signature'].match(/t=(\d+),v1=([a-f0-9]+)/);
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) && Math.abs(Date.now() / 1000 - t) < 300;
Respond with any 2xx status within 15 seconds. Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, so make your handler idempotent: use id to ignore repeats. Redirects aren't followed, and URLs must be public https:// addresses. Test documents never trigger webhooks.
7. Document lifecycle
draft → sent → completed, or declined, voided or expired (after 30 days). The sender is emailed on every signature, decline and completion. When everyone has signed, all parties get the sealed PDF.
8. Signatures and verification
- Document hash: SHA-256 of the markdown (line endings normalised to
\n), fixed at send time. - Signature record: document hash, signer name and email, typed signature, UTC timestamp, IP, user agent, consent text and the previous signature's hash. Canonical JSON → SHA-256 → Ed25519 seal.
- Completion seal: Ed25519 over the hash of all signature hashes.
- Public key:
https://md2sign.com/.well-known/md2sign-seal-key. Anyone can check a document athttps://md2sign.com/verify/{id}, including pasting the original markdown to confirm it is unchanged.
9. Emails recipients receive
From "<Company> via md2sign" <[email protected]>, Reply-To the sender. Subject: <Company> (<sender email>) requests your signature: <title>. The body has the sender's note, a Review & sign button, the expiry date and the full rendered document.