Skip to main content

API reference

Generate certificates, check batch status, and receive webhooks from your own backend. Available on the Business plan.

Authentication

Every request carries your API key as a bearer token. Create and revoke keys from Settings > API. A key is shown in full exactly once, at creation - CertPie only ever stores its hash.

Shell
1curl https://certpie.com/api/v1/templates \2  -H "Authorization: Bearer cp_live_..."

A key starts with cp_live_. There is no separate test-mode key today.

Rate limits

60 requests a minute and 5,000 a day, per key. A request over either limit gets a 429 response with a Retry-After header, in seconds.

1429 Too Many Requests2Retry-After: 42

Identify your workspace

Confirms a key works and which workspace it belongs to - useful for a one-time connection check (it's what the Zapier integration's own "Test" step calls).

GET/api/v1/me
{ "organization": { "id": "b1a2c3d4-...", "name": "Acme Institute", "plan": "business" } }

List templates

Every template this key's workspace can generate from, with the field names each one expects. supportsBackgroundGeneration is false for a template using a gradient color - background generation can't render these yet, so POST /api/v1/certificates/batches rejects a batch for one immediately.

ready is false for a template with no {{recipient_name}} field yet - typically a system template (or a duplicate of one) that hasn't been opened in the editor to add fields. Generating from it would produce certificates with no recipient name on them, so POST /api/v1/certificates/batches rejects it (422) instead. not_ready_reason is present only when ready is false.

GET/api/v1/templates
1{2  "templates": [3    {4      "id": "b1a2c3d4-...",5      "name": "Course completion",6      "fields": ["{{recipient_name}}", "{{recipient_email}}", "{{course_name}}"],7      "supportsBackgroundGeneration": true,8      "ready": true9    },10    {11      "id": "e5f6a7b8-...",12      "name": "Navy Gold Geometric",13      "fields": ["{{recipient_email}}"],14      "supportsBackgroundGeneration": true,15      "ready": false,16      "not_ready_reason": "This template has no recipient name field yet. Duplicate it and add fields in the editor before generating."17    }18  ]19}

Generate certificates

Queues a batch for background generation - the same pipeline the dashboard's bulk-generate wizard uses. Each recipient object is keyed by the field names GET /api/v1/templates returned for that template. {{recipient_name}} is required on every recipient. A template with supportsBackgroundGeneration: false or ready: false is rejected immediately (422) rather than failing later or generating certificates with no name on them.

POST/api/v1/certificates/batches
1Content-Type: application/json2 3{4  "templateId": "b1a2c3d4-...",5  "batchName": "October cohort",6  "recipients": [7    { "{{recipient_name}}": "Jane Doe", "{{recipient_email}}": "jane@example.com", "{{course_name}}": "AI 101" }8  ]9}10 11202 Accepted12{ "batchId": "f9e8d7c6-...", "status": "queued" }

Up to 2,000 recipients per request. A request that would exceed your plan's certificate allowance gets a 402 response instead of a partial batch. A recipient key that matches none of the template's fields - a typo, usually - doesn't fail the request; it comes back as a warning instead, so it's worth checking for:

1202 Accepted2{3  "batchId": "f9e8d7c6-...",4  "status": "queued",5  "warnings": { "unmatchedRecipientKeys": ["{{cours_name}}"] }6}

List batches

Your most recent batches, newest first. Optional limit query parameter (default 25, maximum 100).

GET/api/v1/certificates/batches?limit=10
1{2  "batches": [3    { "id": "f9e8d7c6-...", "name": "October cohort", "status": "completed", "total_count": 50, "generated_count": 50, "sent_count": 0, "error_message": null, "created_at": "2026-10-24T12:00:00.000Z" }4  ]5}

Check batch status

GET/api/v1/certificates/batches/{batchId}
1{2  "batch": {3    "id": "f9e8d7c6-...",4    "name": "October cohort",5    "status": "partial",6    "total_count": 50,7    "generated_count": 47,8    "sent_count": 0,9    "error_message": "3 rows weren't generated.",10    "created_at": "2026-10-24T12:00:00.000Z",11    "skipped_count": 1,12    "blocked_count": 2,13    "failed_count": 0,14    "row_outcomes": [15      { "row_index": 12, "kind": "skipped", "reason": "Skipped: no recipient name provided for this row.", "recipient_name": null },16      { "row_index": 30, "kind": "blocked", "reason": "This row has Hindi (Devanagari) characters that Lora can't print. Change the font or fix the data for this row.", "recipient_name": "Aanya Sharma" }17    ]18  }19}

status is one of draft, generating, completed, partial, failed, or cancelled. Subscribe to the batch.completed webhook instead of polling, where possible.

skipped_count/blocked_count/failed_count break down total_count - generated_count by why each row didn't generate: skipped (no recipient name), blocked (a character the template's font can't print), or a real failure. Every row in that gap also appears in row_outcomes, with its original 0-based row index and a factual reason.

Look up a certificate

Fetches one certificate once it has been generated: its PDF URL, its public verification link, and its status.

GET/api/v1/certificates/{certificateId}
1{2  "certificate": {3    "id": "a1b2c3d4-...",4    "certificate_number": "CERT-2026-00042",5    "recipient_name": "Jane Doe",6    "recipient_email": "jane@example.com",7    "status": "generated",8    "pdf_url": "https://certpie.com/api/storage/certificates/...",9    "verifyUrl": "https://certpie.com/verify/xK9pQ2mN8r",10    "issued_date": "2026-10-24",11    "expiry_date": null,12    "created_at": "2026-10-24T12:00:05.000Z"13  }14}

Webhooks

CertPie can notify your own endpoint when a batch finishes, instead of you polling for status - this is also what powers a Zapier "New batch completed" trigger. Subscribe and unsubscribe with the same API key used above.

POST/api/v1/webhooks/subscriptions
1Content-Type: application/json2 3{ "url": "https://your-app.example.com/hooks/certpie", "event": "batch.completed" }4 5201 Created6{ "id": "c7b6a5d4-...", "secret": "whsec_..." }

The endpoint URL must use https. secret is returned exactly once, here - store it, it's what you use to verify a delivery actually came from CertPie (below). To stop delivery, call DELETE /api/v1/webhooks/subscriptions/{id}.

Delivery and signature

Every delivery is a signed POST, matching the Standard Webhooks shape:

POSThttps://your-app.example.com/hooks/certpie
1webhook-id: evt_...2webhook-timestamp: 17613072003webhook-signature: base64-encoded HMAC-SHA2564 5{6  "event": "batch.completed",7  "data": {8    "batchId": "f9e8d7c6-...",9    "status": "completed",10    "generatedCount": 50,11    "totalCount": 5012  }13}

Verify it by computing HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body} with the secret CertPie gave you when the subscription was created, base64-encoding it, and comparing that to webhook-signature (use a constant-time comparison, not ===).

Connect via Claude (MCP)

Everything above is also available as an MCP (Model Context Protocol) connector, so Claude can generate certificates, check batch status, and manage webhooks directly from a chat. No API key to copy, no terminal - it signs in with your CertPie account the same way any other app you'd connect to Claude does.

https://certpie.com/api/mcp

In Claude, open Settings > Connectors > Add custom connector, paste that URL, and approve the connection when prompted. Claude handles the rest (registration and sign-in) on its own.

Available tools

  • certpie_whoami - identify the connected workspace
  • certpie_list_templates - list templates and their fields
  • certpie_generate_certificates - queue a batch
  • certpie_list_batches - list recent batches
  • certpie_get_batch_status - check one batch
  • certpie_get_certificate - look up one certificate
  • certpie_list_webhooks - list webhook subscriptions
  • certpie_subscribe_webhook - subscribe a webhook endpoint
  • certpie_unsubscribe_webhook - remove a webhook subscription

Errors

Every error response is JSON, shaped the same way regardless of status code:

{ "error": { "code": "TEMPLATE_NOT_FOUND", "message": "Template not found" } }

message is a human-readable sentence - stable in meaning, but not guaranteed to be byte-for-byte identical between API versions. code is the stable identifier to branch your integration on.

  • 400 - the request body or a field in it is invalid
  • 401 - the API key is missing, invalid, or revoked
  • 402 - the request would exceed your plan's certificate allowance
  • 403 - the key's workspace is not on the Business plan, or the key is read-only
  • 404 - the template, batch, certificate, or subscription was not found for this workspace
  • 422 - the template can't be used to generate certificates as it stands
  • 429 - rate limit exceeded; see Retry-After
  • 500 - something went wrong on our end; the request can be retried

Error codes

Match on error.code, not error.message.

  • MISSING_AUTHORIZATION - no (or a malformed) Authorization header
  • INVALID_API_KEY - the key is unrecognized or revoked
  • PLAN_UPGRADE_REQUIRED - the workspace isn't on the Business plan
  • RATE_LIMIT_EXCEEDED - too many requests; see Retry-After
  • READ_ONLY_API_KEY - the key can't perform write actions
  • INVALID_JSON_BODY - the request body isn't valid JSON
  • VALIDATION_ERROR - a field in the request body failed validation (see message for which one)
  • TEMPLATE_NOT_FOUND - no template with that id in this workspace
  • TEMPLATE_NOT_RENDERABLE - the template uses something the background renderer can't draw yet
  • TEMPLATE_NOT_READY - the template has no recipient-name field
  • TEMPLATE_STATIC_TEXT_UNPRINTABLE - the template has fixed text its font can't print
  • PLAN_LIMIT_REACHED - the request would exceed the plan's certificate allowance
  • BATCH_NOT_FOUND - no batch with that id in this workspace
  • ORGANIZATION_NOT_FOUND - the key's workspace couldn't be resolved
  • CERTIFICATE_NOT_FOUND - no certificate with that id in this workspace
  • SUBSCRIPTION_NOT_FOUND - no webhook subscription with that id in this workspace
  • INTERNAL_ERROR - something went wrong on our end; the request can be retried