Pro plan

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_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Base 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 /certificates

Send 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+" }
      }
    ]
  }'
FieldNotes
recipient_nameRequired, up to 150 characters. Any Indian script works.
recipient_emailOptional. If set, the student is emailed their certificate.
courseOptional when the template has a default course.
issue_dateOptional, defaults to today. YYYY-MM-DD or DD/MM/YYYY.
valid_from, valid_toOptional dates. Students get reminders before valid_to.
dataValues 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 counts

Certificate 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."] } } }
StatusCode
401unauthenticated: missing, wrong or revoked key
402pro_required or insufficient_credits (with needed and available)
403organization_suspended: the institute is under review
404not_found: not in your institute
409already_revoked
422validation_failed with rows (by position) or fields
409idempotency_key_reused: the Idempotency-Key was already used with different data (key order and whitespace are ignored)
429rate_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:

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

Need a hand connecting Moodle, WordPress, Google Forms or Zapier? Write to support@ecert.digital.