Skip to main content

API Contracts

:::info Source Sourced from services/certification-service/API_CONTRACTS.md in the documentation repo. :::

1. Surfaces

  • Internal REST for tenant admin + learner UIs.
  • Public REST for unauthenticated verification.

2. Versioning

URL-major (/api/v1/); minor via X-API-Version.

3. Endpoints

3.1 Internal (authenticated)

POST /api/v1/certificates/issue (internal; triggered by worker)
GET /api/v1/certificates/{id}
GET /api/v1/users/{uid}/certificates
POST /api/v1/certificates/{id}/revoke body: { reason, publicReason? }
GET /api/v1/certificates/{id}/audit

GET /api/v1/certificate-templates
POST /api/v1/certificate-templates
GET /api/v1/certificate-templates/{id}
PATCH /api/v1/certificate-templates/{id}
POST /api/v1/certificate-templates/{id}/preview body: { sampleCertificate }

POST /api/v1/certificates/claims/submit (offline claim push)
GET /api/v1/certificates/claims/{id}

3.2 Public (unauthenticated, rate-limited)

GET /api/v1/certificates/verify/{token}
Returns: {
state: 'issued' | 'revoked',
issuedAt, expiresAt?,
userDisplayName,
courseTitle,
tenantPublicName,
tenantLogoUrl?,
proof: { alg, kid, signature },
verificationTokenFingerprint,
publicReason? // if revoked
}

GET /api/v1/.well-known/certification-keys.json (JWKS)

4. Request / Response Schemas

Certificate (response)

{
"data": {
"id": "cert_01H...",
"state": "issued",
"userId": "u_01H...",
"courseId": "crs_01H...",
"courseVersionId": "cv_01H...",
"templateId": "tpl_...",
"issuedAt": "2026-04-15T10:00:00Z",
"expiresAt": "2029-04-15T10:00:00Z",
"evidence": { "completionRecordId": "cmp_..." },
"artifacts": {
"pdfUrl": "https://cdn.ghasi.io/.../cert.pdf?sig=...",
"pngUrl": "https://cdn.ghasi.io/.../cert.png?sig=...",
"openBadgesUrl": "https://cdn.ghasi.io/.../vc.json",
"walletJwsUrl": "https://cdn.ghasi.io/.../wallet.pkpass"
},
"verificationToken": "vt_01H..."
}
}

Verify (public response)

{
"data": {
"state": "issued",
"issuedAt": "2026-04-15T10:00:00Z",
"userDisplayName": "Jane Doe",
"courseTitle": "Compliance 2026",
"tenantPublicName": "ACME Corp",
"tenantLogoUrl": "...",
"proof": { "alg": "EdDSA", "kid": "acme-2026", "signature": "..." },
"verificationTokenFingerprint": "..."
},
"meta": { "requestId": "...", "apiVersion": "v1.0" }
}

5. Error Model

  • resource.not_found (404) — certificate/token missing.
  • resource.gone (410) — certificate revoked (some callers want 410).
  • validation.proof.invalid (422).
  • rate.limited (429).
  • validation.claim.signature_invalid (422) for offline claims.

6. Pagination

Cursor; page[size] max 200 on list endpoints.

7. Rate Limits

  • Public /verify — 60/min per IP; 10k/min per tenant (protects against enumeration).
  • Internal writes per tenant quota.

8. Security

  • JWT required on internal; X-Tenant-Id + tid match.
  • Public endpoints require nothing; verify tokens are unguessable ULIDs + HMAC.
  • Artifact URLs signed with 10-min TTL.

9. Idempotency

Idempotency-Key on issuance/revocation. Duplicate issue requests return existing cert.

10. SLOs

  • Public /verify p95 < 100ms (CDN-cacheable for issued cert).
  • Issuance pipeline p95 < 10s end-to-end (completion event → certificate visible).