Skip to content
How It Works Solutions Use Cases Templates Pricing Clients FAQ Alternatives Verify
Sign In Start Free
Developers

Issue and verify from your own systems

A JSON REST API with scoped API keys, plus signed webhooks that tell your systems what happened to each certificate. API access is included on every plan, including the free Starter plan.

Authentication

Create an API key in the app under Settings, then API keys. Choose the scopes it needs and, optionally, an expiry in days. The full key (it starts with crtfd_live_) is shown once, so store it safely. Send it as a bearer token:

Authorization: Bearer crtfd_live_your_key_here

All endpoints live under https://certifyd.cloud/api/v1 and speak JSON. Successful responses wrap the payload as { "data": ... }. Errors come back as { "error": { "code", "message" } }. Common codes are INVALID_API_KEY, INSUFFICIENT_SCOPE (the response names the missing scope) and CREDIT_LIMIT (HTTP 402, your monthly credits are used up). Requests are rate limited. Back off when you receive HTTP 429.

Scopes

ScopeGrants
events:read, events:writeRead, or create / update / delete, events and their registration settings
attendees:read, attendees:writeRead, or create / update / delete / import, attendees
templates:read, templates:writeRead, or manage, certificate templates
certificates:generateIssue, regenerate and bulk-generate certificates
certificates:read, certificates:writeRead and download certificates, or revoke, delete and send them
badges:read, badges:writeRead, or create / update / delete / issue, badges
analytics:readDashboard and per-event analytics
webhooks:manageManage webhook subscriptions and read their deliveries
API keys cannot create or revoke other API keys: that is only possible from a signed-in session in the app, so a leaked key cannot mint a replacement for itself.

Example: issue a certificate, then verify it

The key needs the scopes templates:read, events:write, attendees:write and certificates:generate. Each issued certificate uses one credit. The examples use jq to pick values out of the JSON.

# 0. Your key and the API address
export CERTIFYD_KEY="crtfd_live_your_key_here"
export API="https://certifyd.cloud/api/v1"

# 1. Pick one of your templates (take the "id" of any entry in data)
curl -s "$API/templates?active=true" \
  -H "Authorization: Bearer $CERTIFYD_KEY" | jq '.data[] | {id, name}'
export TEMPLATE_ID="paste-a-template-id-here"

# 2. Create an event
export EVENT_ID=$(curl -s -X POST "$API/events" \
  -H "Authorization: Bearer $CERTIFYD_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"Safety Training\",\"eventDate\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"templateId\":\"$TEMPLATE_ID\"}" \
  | jq -r '.data.id')

# 3. Add an attendee
export ATTENDEE_ID=$(curl -s -X POST "$API/attendees" \
  -H "Authorization: Bearer $CERTIFYD_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"eventId\":\"$EVENT_ID\",\"firstName\":\"Ana\",\"lastName\":\"Rivera\",\"email\":\"ana@example.com\"}" \
  | jq -r '.data.id')

# 4. Issue the certificate ("sendEmail": false holds back the delivery email).
#    The Idempotency-Key makes a retry of this exact request safe (see below).
export CODE=$(curl -s -X POST "$API/certificates/generate" \
  -H "Authorization: Bearer $CERTIFYD_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d "{\"templateId\":\"$TEMPLATE_ID\",\"eventId\":\"$EVENT_ID\",\"attendeeId\":\"$ATTENDEE_ID\",\"sendEmail\":false}" \
  | jq -r '.data.verificationCode')
echo "Verification code: $CODE"

# 5. Verify it. This endpoint is public: no API key needed.
curl -s "$API/verify/$CODE"

The verify response includes status (valid, expired or revoked), the recipient, the credential, the issuer and the dates. The same code opens the human-readable page at https://certifyd.cloud/verify/<code>. See how to verify a credential.

Retries never issue twice

An attendee holds at most one valid certificate per event and template. If POST /certificates/generate names an attendee who already has one, the API answers HTTP 409 with the code ALREADY_ISSUED and a Location header pointing at the existing certificate; the error body's existing member carries its id, certificateNumber and verificationCode. Nothing is issued, emailed or charged. A revoked or expired certificate does not count, so you can issue a new one after it.

POST /badges/{id}/issue works the same way per attendee. An attendee who already holds the badge comes back in the response with "alreadyIssued": true, and only new badges use credits.

Both endpoints also accept an optional Idempotency-Key header: 1 to 255 printable characters, ideally a UUID that you generate per request. If the same key arrives again with the same body within 24 hours, the API answers HTTP 200 with the original response and the header Idempotent-Replayed: true. The same key with a different body is refused with HTTP 422 IDEMPOTENCY_KEY_REUSED. A repeat that arrives while the first request is still running gets HTTP 409 IDEMPOTENCY_IN_PROGRESS, so retry it a moment later. A request that failed is not remembered, so you can retry it with the same key.

Deleting an issued credential

DELETE /certificates/{id} on a certificate that has been issued revokes it with the reason "Deleted by issuer" (the public verify response also carries revocationReasonCode: "deleted_by_issuer") instead of erasing it, so anyone who scans its QR code sees "revoked", never "not found". Only a draft that was never issued is removed. DELETE /badges/{id} on a badge that has been issued retires it: it leaves your badge list and the badges already issued from it are revoked. An attendee who still has certificates cannot be deleted (HTTP 409 ATTENDEE_HAS_CERTIFICATES), and neither can one who has been issued badges (HTTP 409 ATTENDEE_HAS_BADGES).

Endpoints

Every path below is relative to /api/v1. The API reference lists every operation with its parameters, responses and the scope it needs, and lets you try a call.

AreaEndpoints
EventsGET/POST /events GET/PUT/DELETE /events/{id} GET /events/{id}/attendees GET /events/{id}/certificates GET/PUT /events/{id}/registration
Bulk issuingPOST /events/{id}/certificates/generate POST /events/{id}/certificates/generate/jobs GET /events/{id}/certificates/generate/jobs/{jobId}
AttendeesGET/POST /attendees GET/PUT/DELETE /attendees/{id} GET /attendees/count POST /attendees/import GET /events/{id}/attendees/export
TemplatesGET/POST /templates GET/PUT/DELETE /templates/{id} POST /templates/{id}/duplicate POST /templates/{id}/preview GET /system-templates
CertificatesPOST /certificates/generate GET/DELETE /certificates/{id} GET /certificates/{id}/download POST /certificates/{id}/regenerate POST /certificates/{id}/revoke POST /certificates/{id}/send
BadgesGET/POST /badges GET/PUT/DELETE /badges/{id} POST /badges/{id}/issue GET /badges/{id}/assertions
EmailPOST /email/send-bulk GET /email/deliveries
AnalyticsGET /analytics/dashboard GET /analytics/events/{id}
CreditsGET /credits (credits used, limit and remaining this month)
WebhooksGET/POST /webhooks PUT/DELETE /webhooks/{id} GET /webhooks/{id}/deliveries POST /webhooks/{id}/test
Verification (public)GET /verify/{code} GET /verify/{code}/qr GET /verify/{code}/preview.png GET /verify/{code}/badge.png GET /verify/{code}/badge.svg GET /verify/{code}/certificate.pdf

Webhooks

Subscribe an HTTPS endpoint with POST /webhooks (or in the app under Settings, then Webhooks), choosing which events it receives. Endpoints must use HTTPS and resolve to a public address. The signing secret is returned once, when the subscription is created.

EventSent when
certificate.issuedA certificate is generated, one at a time or in bulk
certificate.sentA certificate is emailed to its recipient
certificate.viewedThe recipient opens their portal link for the first time
certificate.downloadedThe recipient downloads the certificate PDF from the portal
certificate.revokedYou revoke a certificate

You can also send yourself a sample with POST /webhooks/{id}/test, which delivers a webhook.test event.

What you receive

POST https://your-server.example/certifyd-hook
Content-Type: application/json
X-Certifyd-Event: certificate.issued
X-Certifyd-Delivery: 7c0e5b0e-0000-0000-0000-000000000000
X-Certifyd-Timestamp: 1790000000
X-Certifyd-Signature: sha256=3f1c...e9

{ "eventType": "certificate.issued", "timestamp": "...", "data": { "certificateId": "...", "certificateNumber": "...", "eventId": "...", "attendeeId": "...", "status": "...", "verificationCode": "CFD-7X4K-92QM" } }

The signature is a hex HMAC-SHA256 of "{timestamp}.{raw body}" keyed with your subscription secret. Verify it, reject timestamps that are more than a few minutes old, and use X-Certifyd-Delivery to ignore duplicates. It stays the same across retries.

# Recompute the signature on your side (bash + openssl)
printf '%s.%s' "$TIMESTAMP" "$RAW_BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET"

Each delivery times out after 10 seconds. A failed delivery is tried up to 5 times in total, after 1 minute, 5 minutes, 30 minutes and 2 hours. A subscription that keeps failing is switched off after 10 failed attempts in a row.

What is not available

There are no official client libraries. The OpenAPI document is published at /openapi/v1.json, so you can generate a client in your own language. There is no native Zapier or Make connector either: any tool that can send HTTPS requests and receive webhooks can integrate with the endpoints above.

§In witness whereof

Issue your first certificate today

Set up takes about ten minutes. Free for 12 credentials a month, no card required.