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.
curl https://certpie.com/api/v1/templates \
-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.
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.
GET /api/v1/templates
{
"templates": [
{
"id": "b1a2c3d4-...",
"name": "Course completion",
"fields": ["{{recipient_name}}", "{{recipient_email}}", "{{course_name}}"]
}
]
}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.
POST /api/v1/certificates/batches
Content-Type: application/json
{
"templateId": "b1a2c3d4-...",
"batchName": "October cohort",
"recipients": [
{ "{{recipient_name}}": "Jane Doe", "{{recipient_email}}": "jane@example.com", "{{course_name}}": "AI 101" }
]
}
202 Accepted
{ "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.
List batches
Your most recent batches, newest first. Optional limit query parameter (default 25, maximum 100).
GET /api/v1/certificates/batches?limit=10
{
"batches": [
{ "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" }
]
}Check batch status
GET /api/v1/certificates/batches/{batchId}
{
"batch": {
"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"
}
}status is one of draft, generating, completed, partial, failed, or cancelled. Subscribe to the batch.completed webhook instead of polling, where possible.
Look up a certificate
GET /api/v1/certificates/{certificateId}
{
"certificate": {
"id": "a1b2c3d4-...",
"certificate_number": "CERT-2026-00042",
"recipient_name": "Jane Doe",
"recipient_email": "jane@example.com",
"status": "generated",
"pdf_url": "https://certpie.com/api/storage/certificates/...",
"verifyUrl": "https://certpie.com/verify/xK9pQ2mN8r",
"issued_date": "2026-10-24",
"expiry_date": null,
"created_at": "2026-10-24T12:00:05.000Z"
}
}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
Content-Type: application/json
{ "url": "https://your-app.example.com/hooks/certpie", "event": "batch.completed" }
201 Created
{ "id": "c7b6a5d4-...", "secret": "whsec_..." }The endpoint URL must use https. secretis 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:
POST https://your-app.example.com/hooks/certpie
webhook-id: evt_...
webhook-timestamp: 1761307200
webhook-signature: base64-encoded HMAC-SHA256
{
"event": "batch.completed",
"data": {
"batchId": "f9e8d7c6-...",
"status": "completed",
"generatedCount": 50,
"totalCount": 50
}
}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 ===).
Errors
Every error response is JSON, shaped the same way regardless of status code:
{ "error": { "message": "Template not found" } }400- the request body or a field in it is invalid401- the API key is missing, invalid, or revoked402- the request would exceed your plan's certificate allowance403- the key's workspace is not on the Business plan404- the template, batch, or certificate was not found for this workspace429- rate limit exceeded; seeRetry-After