REST API
The v1 verification API answers read-only questions about Certichain credentials over HTTPS. It needs no wallet. Every success answer is signed, so a caller who pins the deployment's signer address can trust the answer even through caches.
- Base path:
/api/v1on the deployment's site. - Machine-readable description:
GET /api/v1/openapi.json(OpenAPI 3.1). - Signed answer format: API envelope.
- Server internals: Verification API (server side).
The API is built but off on the Sepolia deployment until the owner sets its signer key. While off, every endpoint answers 503 CONFIG_MISSING. The signer address to pin is published by the deployment owner as NEXT_PUBLIC_API_SIGNER.
Before you call
- Get the signer address from the deployment owner over a channel you trust, and hard-code it.
- Optionally get the chain id and the registry addresses of the contract sets you accept, and pin those too.
- Send a fresh random 32-byte nonce with every request (
?nonce=0x...) and check that the answer echoes it. - Check every answer as described in API envelope, or use the SDK, which does it for you.
Authentication and limits
No key is needed. An API key gives a higher budget:
Authorization: Bearer <key>
| Tier | GET per hour | POST /verify per hour | Counted per |
|---|---|---|---|
| Anonymous | 600 | 60 | IP address (IPv6 per /64) |
| Standard key | 6,000 | 600 | Key |
A malformed or unknown key is refused with 401, never served as anonymous. Counted answers carry RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset; a 429 carries Retry-After. Keys are issued by the deployment owner (npm run api:key); the server stores only their SHA-256 hashes.
GET /credentials/{id}
Chain facts about one credential, looked up in every contract set the deployment lists (active set first).
| Parameter | Where | Rule |
|---|---|---|
id | path | 0x plus 64 hex characters |
nonce | query, optional | 0x plus 64 hex characters |
curl "https://certichain.example/api/v1/credentials/0x3333333333333333333333333333333333333333333333333333333333333333?nonce=0x5555555555555555555555555555555555555555555555555555555555555555"
data when found:
| Field | Type | Meaning |
|---|---|---|
id | hex | The credential id |
found | true | |
issuer | address | The key that signed the credential |
currentIssuer | address | The key now in charge of that issuer, after any key retirements |
issuerRegistered | boolean | currentIssuer is a registered issuer |
issuerSuspended | boolean | currentIssuer is suspended (all its credentials then count as suspended) |
issuerRetired | boolean | The signing key was retired in favour of a successor |
recordType | number | Record type id |
issuedAt | number | Unix seconds |
validUntil | number | Unix seconds, 0 for no expiry |
status | string | active, suspended, revoked or superseded |
expired | boolean | validUntil is set and passed when the answer was made |
When not found, data is { "id": "0x...", "found": false }. "Not found" is only answered when every listed set was read; if one could not be read the answer is 503.
A credential is in good standing when it is found, active, not expired, and its issuer is registered and not suspended. Whether you accept that issuer as an institution is your decision.
POST /verify
Verifies a verifiable credential file or an Open Badge file exported from Certichain, both offline (signatures, claims) and against the chain.
curl -X POST "https://certichain.example/api/v1/verify?nonce=0x5555555555555555555555555555555555555555555555555555555555555555" \
-H "Content-Type: application/json" \
--data-binary @credential.vc.json
| Rule | Failure |
|---|---|
Content-Type starts with application/json | 415 (not counted against the budget) |
| Body at most 65,536 bytes | 413 |
| The file parses as a Certichain VC or Open Badge | 400 |
The file's registry (domain.verifyingContract) is listed by this deployment | 422 REGISTRY_NOT_LISTED |
data fields:
| Field | Meaning |
|---|---|
kind | vc or open-badge |
id | The credential id in the file |
offline | scope, consistency (do the claims match the signed package), mismatches, id, details, signature (valid or needs-contract-check), expired |
online | chain, credential, anchor, issuerRegistered, active, signature, holder (not-claimed, match, mismatch or unbound) |
lists | Status list results, when the file has status list entries |
standardProof | Result of a badge's standard eddsa-rdfc-2022 proof, when present |
fullyVerified | true only when every check passed |
limits | Plain sentences about what this verdict does not prove |
fileSha256 | SHA-256 of the exact body received; compare it with your own hash of the file |
The answer is always Cache-Control: no-store.
GET /status
Who is answering: { apiVersion, chainId, registry, signer, sets: [{ registry, active }] }. Use it once to confirm the signer and list the contract sets, then pin them.
GET /openapi.json
The OpenAPI 3.1 document, unsigned, Cache-Control: public, max-age=86400. It declares servers /api/v1, optional bearer auth, and the Meta, Signature, CredentialData, StatusData, VerifyData and ErrorResponse schemas. The SDK's types are tested against it.
Caching and CORS
| Request | Cache-Control |
|---|---|
POST /verify, or any request with a nonce | no-store |
| With an API key | private, no-store |
Anonymous GET without a nonce | public, max-age=15 |
All endpoints send Access-Control-Allow-Origin: *, so a browser page on another site can call the API. OPTIONS preflights are answered with 204.
Errors
Errors are plain JSON and not signed: { "error": { "code": "...", "message": "..." } }. Treat an unsigned answer as no answer.
| Status | Code | Meaning |
|---|---|---|
| 400 | BAD_REQUEST | Bad id, nonce, body or file |
| 401 | UNAUTHORIZED | Malformed or unknown API key |
| 413 | PAYLOAD_TOO_LARGE | Body over 65,536 bytes |
| 415 | UNSUPPORTED_MEDIA_TYPE | POST /verify not sent as JSON |
| 422 | REGISTRY_NOT_LISTED | The file names a registry this deployment does not answer for |
| 429 | RATE_LIMITED | Budget used up; see Retry-After |
| 503 | CONFIG_MISSING | The API is off on this deployment |
| 503 | UPSTREAM_ERROR | The chain or the counter could not be read; retry after Retry-After |
Known limits
- One
POST /verifymakes several chain reads (about five) with a 10 second timeout each. - For a credential issued by a contract wallet, the server calls the wallet address named in the file to check the signature.
- Answers can be up to
dataMaxAgeSecondsbehind the chain (15 seconds by default). For a decision that must see a revocation immediately, read the chain directly.
Source files
src/app/api/v1/*/route.tssrc/lib/server/api/handler.ts,credential-lookup.ts,verify-file.tssrc/lib/api/openapi.ts,src/lib/api/envelope.tsdocs/api/README.md
Tests
src/lib/server/api/handler.test.ts,src/lib/api/openapi.test.tsinterop/test/api-answers.test.mjs- E2E:
e2e/journeys/verification-api.journey.ts