md2sign

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

  1. Validate (no account): POST /api/v1/validate { "markdown": "..." }
  2. Preview (no account): POST /api/v1/render { "markdown": "...", "view": "signing_page" | "email", "format": "html" }. Show the HTML to your human.
  3. Create an account for your human: POST /api/v1/signup { "name", "email", "company_name" } returns api_key and checkout_urls. The human gets a confirmation email. Give them a checkout_url to subscribe.
  4. Draft: POST /api/v1/documents { "markdown": "..." } with Authorization: Bearer <api_key>. Update it with PATCH /api/v1/documents/{id} as often as you like.
  5. Test send (free): POST /api/v1/documents/{id}/test-send emails a watermarked copy to the account owner.
  6. Send: POST /api/v1/documents/{id}/send { "message": "optional note" }. Requires a confirmed email and an active plan.
  7. Track: GET /api/v1/documents/{id} and GET /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/me returns team (your role and the admin's email) and pro_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 at https://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.