Waiverly API, as shipped in the download
Waiverly REST API & Webhooks (v2.0)
Waiverly's API is read-mostly, deliberately. It answers the questions an integration legitimately has — what templates exist, who has signed, is this person covered, is this record intact — and it does not let anything sign on a person's behalf.
Why signing is not an endpoint
A waiver's legal weight comes from a specific person, in front of a specific document version, drawing a signature and ticking the acknowledgements themselves. Waiverly captures exactly that: the signer's identity, the template version, the acknowledgements, the signature image, and a tamper-evident record_hash over all of it.
An endpoint that let a server assert "consider this person to have signed" would manufacture records nobody actually signed — and would hollow out every waiver in the table, including the real ones. So there is no POST /api/waivers, and signing stays at /w/{token} (share link) and /k/{token} (kiosk), where a human does it. A test asserts the endpoint does not exist and that Waiver::create() is never reachable from the API layer.
Authentication
Authorization: Bearer apk_...
Endpoints
| Method & path | What it does | |
|---|---|---|
GET /api/ping | Verify a key | |
GET /api/templates | Templates and their public signing URLs | |
GET /api/waivers | ?signer= (name or email substring) · `?status=valid\ | expired` |
GET /api/waivers/{code} | One waiver plus an integrity_ok verdict | |
GET /api/openapi.json | OpenAPI 3.0 |
Check someone is covered before they turn up
curl -H "Authorization: Bearer apk_..." \
"https://waivers.example.com/api/waivers?signer=ada@example.com&status=valid"
Verify a record has not been altered
curl -H "Authorization: Bearer apk_..." https://waivers.example.com/api/waivers/ABC123
integrity_ok re-runs the product's own Waiver::integrityOk() against the stored template version — the same check the verify page uses. false means the record or its template version no longer hashes to record_hash.
What the API will not return
- The signature image. Neither
signaturenorguardian_signatureis ever serialized. It is the artefact at the centre of a legal record and an integration has no reason to hold a copy. - The signer's IP. Captured for the record; never handed out.
Both are asserted by tests (no_signature_image, no_signer_ip).
MCP — the agent surface (new in 3.0)
Waiverly speaks MCP at POST /mcp, authenticated with the API key you already have:
Authorization: Bearer <your api key>
GET /mcp answers 405 with Allow: POST, so a misconfigured client is told what to do rather than concluding you have no MCP support.
Connect it to an assistant
Mint the key in the app first: Settings → API keys. Choose Read only when the assistant should answer questions but never change anything — the endpoint then lists only the read tools and refuses the rest by name, so a careless prompt cannot write. Full access behaves as before.
Every client needs the same three facts, and nothing in the handshake is vendor-specific:
| The address | https://waivers.example.com/mcp |
| The key | header Authorization: Bearer apk_xxxx |
| The transport | MCP over streamable HTTP, stateless |
Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:
claude mcp add --transport http waiverly https://waivers.example.com/mcp \
--header "Authorization: Bearer apk_xxxx"
ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):
{
"type": "mcp",
"server_label": "waiverly",
"server_url": "https://waivers.example.com/mcp",
"authorization": "apk_xxxx",
"require_approval": "never"
}
Own Your AI reads a list of servers in this shape:
{
"mcpServers": [
{ "id": "waiverly", "name": "Waiverly", "url": "https://waivers.example.com/mcp",
"token": "apk_xxxx", "enabled": true }
]
}
**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"* — not an mcpServers array. Pasted as-is, the block above will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.
A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:
curl -X POST https://waivers.example.com/mcp \
-H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: Waiverly implements the protocol, not an integration with a particular vendor.
Tools — all read-only, on purpose
| Tool | What it does |
|---|---|
list_templates | Templates with their current version and expiry policy |
list_waivers | Signed waivers; filter by template, status or signing date |
waiver_detail | One waiver with the template text exactly as it was signed |
signing_link | The public signing URL for a template (the existing public link) |
stats | Counts by status and template, plus what expires soon |
Nothing here writes a waiver, and that is deliberate. A signed waiver is evidence of what a person agreed to on a given day. There is no tool to create, edit or void one, because an agent should not be able to manufacture a signature. signing_link is as close as it gets: it returns the URL a human signs at.
Status is computed, never stored
valid · expired (past its expiry date) · superseded (the template moved on and that template requires re-signing). The API calls the product's own status rule; it never decides separately whether a waiver still stands.
The snapshot rule
waiver_detail returns signed_text from the stored template version, not the live template. Edit your template afterwards and the waiver still shows what was actually agreed. The PDF obeys the same rule.
Signature images are never returned by any tool, and never enter the audit trail.
curl -s -X POST http://your-install/mcp \
-H "Authorization: Bearer <key>" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Calendar feed (new in 3.0)
GET /calendar.ics?t=<token> is a read-only iCal feed of upcoming expiries — subscribe in Google Calendar, Outlook or Apple Calendar and re-signing lands in the diary. Mint the token in Settings → Expiry calendar; it is shown once, and minting a new one invalidates the old.
Erasure, honestly (new in 3.0)
Subject export is always available. Anonymising is refused while a waiver is still valid — erasing the signer from a waiver you are still relying on destroys the only thing it is for. Once expired or superseded, anonymising blanks the signer's details and the signature image while keeping the evidentiary skeleton: the reference code, the dates, and which template version was signed.
Webhooks
| Event | Fires when |
|---|---|
waiver.signed | Someone completes a signature (link or kiosk) |
Payload carries identity, dates and record_hash — never the signature image, never the IP.
X-Waiverly-Event: waiver.signed
X-Waiverly-Signature: sha256=<hmac_sha256(raw_body, your_webhook_secret)>
Import — N/A, and why
There is no CSV import. Importing waivers would mean **inserting signed legal records for signatures nobody witnessed**: the record_hash chain would be meaningless, and a row that looks like a signed waiver but was typed in from a spreadsheet is worse than no row at all. Templates are authored in the app because each edit creates a new immutable version that signatures bind to.