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+tidmatch. - 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
/verifyp95 < 100ms (CDN-cacheable for issued cert). - Issuance pipeline p95 < 10s end-to-end (completion event → certificate visible).