REST API and webhooks
Issue certificates automatically when a student finishes a course in your LMS, a Google Form or your own website. The API, webhooks and Open Badges export are included in Pro at no extra cost.
Authentication
The owner or an admin creates keys on the API and webhooks page of the dashboard. A key is shown once; we store only a hash of it. Send it as a bearer token on every request:
Authorization: Bearer ecd_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxBase URL: https://app.ecert.digital/api/v1. Requests and responses are JSON. Keys stop working when they are revoked or when the institute's Pro plan lapses (you get 402 pro_required).
List templates
GET /templates{
"data": [
{ "id": 12, "name": "Course completion", "default_course": "Advanced Excel", "custom_fields": ["grade", "roll_no"] }
]
}Issue certificates
POST /certificatesSend up to 500 certificates per request. Every row is checked with the same rules as a CSV upload. If any row has an error, or your credits would go below zero, nothing is issued. Each certificate uses one credit.
curl https://app.ecert.digital/api/v1/certificates \
-H "Authorization: Bearer $ECERT_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: moodle-course-7-batch-2026-10" \
-d '{
"template_id": 12,
"certificates": [
{
"recipient_name": "Priya Sharma",
"recipient_email": "priya@example.com",
"course": "Advanced Excel",
"issue_date": "2026-10-19",
"valid_to": "2028-10-18",
"data": { "grade": "A+" }
}
]
}'| Field | Notes |
|---|---|
recipient_name | Required, up to 150 characters. Any Indian script works. |
recipient_email | Optional. If set, the student is emailed their certificate. |
course | Optional when the template has a default course. |
issue_date | Optional, defaults to today. YYYY-MM-DD or DD/MM/YYYY. |
valid_from, valid_to | Optional dates. Students get reminders before valid_to. |
data | Values for the template's custom fields (see custom_fields). Unknown keys are ignored. |
For a single certificate you can also send the fields at the top level, next to template_id.
Idempotency: send an Idempotency-Key header (up to 100 characters) so a retried request never issues twice. Repeating a key with the same body returns the original batch with status 200. Reusing a key with a different body is refused with 409 idempotency_key_reused, so use a new key for each distinct request.
The response is 202 Accepted. Certificates start as pending and become issued a few seconds later, once the PDF is rendered. Use a webhook or poll the batch.
{
"batch": { "id": 381, "status": "processing", "source": "api", "total": 1,
"counts": { "pending": 1, "issued": 0, "failed": 0, "revoked": 0 } },
"certificates": [
{ "id": "ECD-7K3M-Q9TX", "status": "pending", "recipient_name": "Priya Sharma",
"course": "Advanced Excel", "issue_date": "2026-10-19", "valid_to": "2028-10-18",
"data": { "grade": "A+" },
"verify_url": "https://ecert.digital/verify/ECD-7K3M-Q9TX",
"credential_url": "https://app.ecert.digital/api/v1/credentials/ECD-7K3M-Q9TX", ... }
]
}Status
GET /certificates/ECD-7K3M-Q9TX one certificate
GET /certificates?batch_id=381&status=issued&per_page=100 list, newest first
GET /batches/381 batch progress and countsCertificate statuses: pending, issued, failed (rendering failed; contact support), revoked.
Revoke
POST /certificates/ECD-7K3M-Q9TX/revoke
{ "reason": "Issued in error" }The verify page then shows the certificate as revoked, and the student is emailed. Revoking twice returns 409 already_revoked.
Errors and limits
Errors look like this:
{ "error": { "code": "validation_failed",
"message": "1 certificate(s) have errors. Nothing was issued.",
"rows": { "1": ["Name is missing.", "Email address is not valid."] } } }| Status | Code |
|---|---|
| 401 | unauthenticated: missing, wrong or revoked key |
| 402 | pro_required or insufficient_credits (with needed and available) |
| 403 | organization_suspended: the institute is under review |
| 404 | not_found: not in your institute |
| 409 | already_revoked |
| 422 | validation_failed with rows (by position) or fields |
| 409 | idempotency_key_reused: the Idempotency-Key was already used with different data (key order and whitespace are ignored) |
| 429 | rate_limited: wait for the Retry-After seconds |
Each key may make 120 requests a minute.
Webhooks
Add up to 5 HTTPS endpoints on the dashboard and pick the events you want:
certificate.issued: the PDF is ready and the student has been emailedcertificate.revokedbatch.completed: every certificate in a batch (API, CSV or single) has finished
We POST JSON like this, with headers Ecert-Event, Ecert-Delivery (unique per event, use it to ignore duplicates) and Ecert-Signature:
{
"id": "0b8e1f2c-6d1a-4a8e-9a57-3f1d2b7c9e10",
"type": "certificate.issued",
"created_at": "2026-10-19T10:15:03+05:30",
"data": { "certificate": { "id": "ECD-7K3M-Q9TX", "status": "issued", ... } }
}Answer with any 2xx status within 10 seconds. Otherwise we retry after about 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. Every attempt appears in the delivery log, where you can also send a test event or retry a failed delivery.
Checking the signature
Ecert-Signature looks like t=1792390503,v1=5257a8…. Compute an HMAC-SHA256 of t + "." + raw body with your endpoint's signing secret and compare it with v1. Reject events older than 5 minutes.
// PHP
[$t, $v1] = sscanf($_SERVER['HTTP_ECERT_SIGNATURE'], 't=%d,v1=%s');
$body = file_get_contents('php://input');
$ok = hash_equals(hash_hmac('sha256', $t . '.' . $body, $secret), $v1) && abs(time() - $t) < 300;// Node.js
const [t, v1] = (req.get('Ecert-Signature') || '').split(',').map(p => p.split('=')[1] || '');
const expected = Buffer.from(crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex'));
const given = Buffer.from(v1);
// timingSafeEqual throws if the lengths differ, so check that first.
const ok = given.length === expected.length && crypto.timingSafeEqual(expected, given)
&& Math.abs(Date.now() / 1000 - Number(t)) < 300;Open Badges 3.0 credentials
Every issued certificate from a Pro institute is also available as an Open Badges 3.0 credential, which is a W3C Verifiable Credential. Students get a download link in their email, and the verify page links to it. It is public and needs no key:
GET https://app.ecert.digital/api/v1/credentials/ECD-7K3M-Q9TX JSON
GET https://app.ecert.digital/api/v1/credentials/ECD-7K3M-Q9TX?download=1 as a .json file- Signed with an
eddsa-jcs-2022Data Integrity proof (Ed25519). - Each institute is a
did:webissuer, for exampledid:web:app.ecert.digital:issuers:sunrise-academy, whose DID document is athttps://app.ecert.digital/issuers/sunrise-academy/did.json. - The student's email is stored as a salted SHA-256 hash, never in clear.
- Revoked certificates return
410 Gone. The verify page is always the source of truth for current status.
Need a hand connecting Moodle, WordPress, Google Forms or Zapier? Write to support@ecert.digital.