# 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 ]] [[signature: First Last | 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: ``` classic (default) | modern | formal ``` Without a title directive, the first `# Heading` is the title. ### Example ```markdown # 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 | role: Director]] [[signature: Jane Citizen ]] ``` ## 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 `. 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`. ```bash curl -s https://md2sign.com/api/v1/validate \ -H "Content-Type: application/json" \ -d '{"markdown":"# NDA\n\nTerms...\n\n[[signature: Jane Citizen ]]"}' ``` ## 3. MCP Add the server to any MCP client that supports remote streamable HTTP servers: ```json { "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 `. 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 ```json { "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](#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: ```json { "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": "jason@acme.com" }, "recipients": [ { "name": "Jane Citizen", "email": "jane@example.com", "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=,v1=` headers. `v1` is the HMAC-SHA256 of `"."`, keyed with the endpoint's signing secret. Compare it in constant time and reject timestamps older than 5 minutes. ```php [$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); ``` ```js 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 `" via md2sign" `, Reply-To the sender. Subject: ` () requests your signature: `. The body has the sender's note, a **Review & sign** button, the expiry date and the full rendered document.