Certora API, as shipped in the download
Certora REST API, MCP & Webhooks (v3.0)
Certora ships a JSON REST API and signed webhooks so your LMS, course platform or HR system can issue certificates programmatically — and so anyone can verify a certificate by code without an account.
The batch endpoint is the point. A cohort finishes a course, your LMS makes one call with the whole roster, and every learner gets a certificate with its own verification code.
Authentication
Create a key in Settings → API & Webhooks. Send it on every call:
Authorization: Bearer apk_...
The key is shown once at creation and stored as a SHA-256 hash. Revoke any time.
One endpoint is deliberately public: GET /api/verify/{code} needs no key. A verification code that only its issuer can check would be pointless — employers and registrars must be able to verify. That endpoint reuses the public verify page's per-IP throttle (20 lookups / 60 s), so it cannot be used to guess codes.
Endpoints
| Method & path | What it does |
|---|---|
GET /api/ping | Verify a key |
GET /api/templates | Certificate templates with their tokens and validity period |
GET /api/certificates | Newest 200 issued certificates; ?recipient= filters by name (substring) |
POST /api/certificates | Issue one certificate or a whole batch |
GET /api/verify/{code} | PUBLIC — verify a code, no key required |
GET /api/openapi.json | OpenAPI 3.0 description of everything above |
Issue one
curl -X POST https://certs.example.com/api/certificates \
-H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
-d '{"template_id":1,"recipient":{"name":"Ada Lovelace","course":"Fire Safety"}}'
Issue a batch (the LMS case)
curl -X POST https://certs.example.com/api/certificates \
-H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
-d '{"template_id":1,"issue_date":"2026-06-30","recipients":[
{"name":"Ada Lovelace","course":"Fire Safety"},
{"name":"Grace Hopper","course":"Fire Safety"},
{"name":"Alan Turing","course":"Fire Safety"}]}'
Returns 201 with every issued certificate, each carrying its own code, verify_url and pdf_url. A batch is capped at 2000 recipients per call.
Validation is all-or-nothing. Every recipient is checked before anything is written, so a bad row never leaves you with half a cohort certified and a 422 you cannot interpret.
Recipients are accepted inline — you do not have to create recipient records first. Your LMS already owns the roster.
Verify (public)
curl https://certs.example.com/api/verify/ABCD-EFGH-JKMN
{ "found": true, "valid": true, "status": "valid",
"code": "ABCDEFGHJKMN", "display_code": "ABCD-EFGH-JKMN",
"recipient_name": "Ada Lovelace", "course": "Fire Safety",
"issuer": "Ridgeline Training", "issue_date": "2026-06-30", "expires_at": "2029-06-30" }
status is one of valid, expired, revoked — computed by the same engine the web verify page uses. Unknown codes return 404 with {"found": false}. Too many lookups return 429.
MCP (Model Context Protocol) — new in 3.0
Certora speaks MCP over stateless streamable HTTP at POST /mcp. Authentication is the same revocable API key as the REST API (Authorization: Bearer <key>), and a key carries its owner's role, so an agent can never do more than the person whose key it is holding.
GET /mcp answers 405 with Allow: POST — deliberately without the key check, so a probing client is told "right endpoint, wrong verb" instead of a bare 401 that reads as "no MCP here".
POST /mcp {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}
POST /mcp {"jsonrpc":"2.0","id":2,"method":"tools/list"}
POST /mcp {"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"issue_certificates",
"arguments":{"template_id":1,"recipients":[4,5,6]}}}
| Tool | Writes? | What it does |
|---|---|---|
list_templates | no | Templates with orientation, validity period and how many each has issued |
template_detail | no | One template, its merge tokens, and the certificates issued from it |
list_certificates | no | The register; filter by template_id, status, search |
issue_certificates | yes | The batch operation — one certificate per recipient, each with its own code and a snapshotted expiry |
verify_certificate | no | The same lookup the public /verify page performs |
expiring_report | no | Expiring within N days, plus those already expired |
Argument hygiene. Tool arguments take the same scalar guard as the REST body: a value may not be an array or object unless the tool allowlists it. issue_certificates allowlists exactly one — recipients — because that is the list it exists to take. Unknown arguments are refused by name, and enum values are refused with the list of what is accepted.
One path, one answer. issue_certificates calls the same writer the Generate screen calls, so the merge, the code generation and the expiry snapshot cannot drift between surfaces. A certificate issued by an agent verifies identically through verify_certificate, GET /api/verify/{code} and the public page. verify_certificate returns the same shape this REST API already documents.
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://certs.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 certora https://certs.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": "certora",
"server_url": "https://certs.example.com/mcp",
"authorization": "apk_xxxx",
"require_approval": "never"
}
Own Your AI reads a list of servers in this shape:
{
"mcpServers": [
{ "id": "certora", "name": "Certora", "url": "https://certs.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://certs.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: Certora implements the protocol, not an integration with a particular vendor.
Webhooks
Add a receiver in Settings → API & Webhooks and choose events:
| Event | Fires when |
|---|---|
certificate.issued | A certificate is issued — from Generate, the REST API or /mcp |
certificate.revoked | A certificate is revoked — one at a time or in bulk from the register (once per certificate; revoking one already revoked sends nothing) |
A batch of N recipients fires N separate certificate.issued events — one per certificate, each with its own code. That keeps a single event shape whether you issued one or a thousand.
Every delivery is signed:
X-Certora-Event: certificate.issued
X-Certora-Signature: sha256=<hmac_sha256(raw_body, your_webhook_secret)>
Compare with hash_hmac('sha256', $rawBody, $secret) and reject on mismatch. Deliveries and their response codes are logged in the same settings page.
Errors
| Code | Meaning |
|---|---|
401 | Missing, unknown or revoked key |
404 | No such certificate / code |
422 | Validation — the detail field names the offending field |
429 | Verification lookups throttled (public endpoint only) |