Endpoints
Full REST endpoint reference — batches, credentials, tenants, webhooks — with request and response shapes.
All endpoints under https://api.learncoin.me/v1/. All require Authorization: Bearer lrn_test_… or lrn_live_…. All accept and return application/json.
Note. This page is the hand-written reference. The authoritative machine-readable spec (OpenAPI 3.1) will ship at
https://learncoin.me/openapi.yaml— that's the surface Cursor, Copilot, and Claude Code consume. Until then, this page is the canonical contract.
Batches
A batch is a group of credentials issued together. Every batch's credentials share the same anchoring transaction and the same issuance timestamp.
POST /v1/batches
Create a batch of one or more credentials. Signing and anchoring happen asynchronously — the endpoint returns immediately.
Request body
{
"credentials": [
{
"recipient": {
"id": "urn:uuid:4e0c7f2e-6b1a-4f5a-9c8e-8a1b2c3d4e5f",
"name": "Ada Lovelace",
"email": "[email protected]"
},
"achievement": {
"id": "https://yourschool.edu/credentials/intro-ml",
"name": "Introduction to Machine Learning",
"description": "Completed the 12-week introductory course with distinction.",
"criteria": {
"narrative": "Passing grade on final project plus peer-reviewed submission."
},
"alignment": [
{
"targetFramework": "ESCO",
"targetCode": "S1.4.0",
"targetName": "Machine learning",
"targetUrl": "https://esco.ec.europa.eu/en/classification/skill?uri=http%3A%2F%2Fdata.europa.eu%2Fesco%2Fskill%2FS1.4.0"
}
]
},
"issuanceDate": "2026-04-23T12:00:00Z",
"expirationDate": "2031-04-23T12:00:00Z"
}
]
}Required fields
| Field | Type | Notes |
|---|---|---|
credentials[].recipient.id | string | urn:uuid:… — pseudonymous recipient ID. |
credentials[].recipient.name | string | Display name; stored off-chain, redactable on erasure. |
credentials[].achievement.name | string | Title of the achievement. |
credentials[].achievement.description | string | Brief description. |
credentials[].issuanceDate | string | ISO 8601 UTC. |
Optional fields
| Field | Type | Notes |
|---|---|---|
credentials[].recipient.email | string | Off-chain only. Used for magic-link delivery if enabled. |
credentials[].achievement.id | string | Stable URL identifying the achievement definition. |
credentials[].achievement.criteria.narrative | string | Free-form criteria prose. |
credentials[].achievement.alignment[] | array | OB 3.0 alignment entries (ESCO, O*NET, custom frameworks). |
credentials[].expirationDate | string | ISO 8601 UTC. Omit for non-expiring credentials. |
credentials[].evidence[] | array | Optional evidence references (URLs, narrative). |
Headers
Idempotency-Key: <any-string-up-to-255-chars>— recommended. Retries with the same key within 24 h return the original response. See Idempotency.
Response — 202 Accepted
{
"id": "bat_01HXYZABCDEF1234567890ABCD",
"status": "pending",
"credentials_count": 1,
"created_at": "2026-04-23T12:00:01.234Z",
"environment": "test"
}statustransitions:pending→signed→anchored(typical path, ≤ 60 s on testnet; ≤ 3 min on mainnet). Or:pending→failedwitherrorpopulated.- Subscribe to
batch.anchoredwebhooks rather than polling. See Webhooks.
GET /v1/batches/{id}
Fetch the current state of a batch.
Response — 200 OK
{
"id": "bat_01HXYZ…",
"status": "anchored",
"credentials_count": 1,
"created_at": "2026-04-23T12:00:01.234Z",
"anchored_at": "2026-04-23T12:00:58.101Z",
"anchor_transaction": {
"chain": "base-sepolia",
"hash": "0xabcd…ef12",
"block_number": 14582301,
"explorer_url": "https://sepolia.basescan.org/tx/0xabcd…ef12"
},
"merkle_root": "0x7c2f…91a8",
"credentials": [
{
"id": "crd_01HXYZ…",
"recipient_id": "urn:uuid:4e0c7f2e-…",
"verify_url": "https://learncoin.me/c/crd_01HXYZ…"
}
]
}GET /v1/batches
List batches, newest first.
Query params
| Param | Type | Notes |
|---|---|---|
cursor | string | Pagination cursor from a prior response. |
limit | integer | 1–100, default 25. |
status | string | Filter: pending | signed | anchored | failed. |
Response — 200 OK
{
"data": [ /* array of batch objects as above */ ],
"next_cursor": "eyJsYXN0X2lkIjoi…",
"has_more": true
}Credentials
GET /v1/credentials/{id}
Fetch a single credential as signed JSON-LD. This is the same document the public /c/{id} page renders and any third-party verifier will consume.
Response — 200 OK
{
"id": "crd_01HXYZ…",
"verify_url": "https://learncoin.me/c/crd_01HXYZ…",
"status": "anchored",
"revoked": false,
"erased": false,
"signed_credential": {
"@context": [
"https://www.w3.org/ns/credentials/v2",
"https://w3id.org/blockcerts/v3",
"https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json"
],
"type": ["VerifiableCredential", "OpenBadgeCredential"],
"issuer": {
"id": "did:web:learncoin.me#tenant-01HXYZ",
"name": "Example University"
},
"issuanceDate": "2026-04-23T12:00:00Z",
"credentialSubject": {
"id": "urn:uuid:4e0c7f2e-…",
"name": "Ada Lovelace",
"type": ["AchievementSubject"],
"achievement": { /* … OB 3.0 Achievement … */ }
},
"proof": {
"type": "MerkleProof2019",
"created": "2026-04-23T12:00:58.101Z",
"verificationMethod": "did:web:learncoin.me#tenant-01HXYZ-key-1",
"proofValue": "…base58…"
}
}
}See /docs/concepts/credentials for a field-by-field anatomy.
POST /v1/credentials/{id}/revoke
Mark a credential as revoked. The credential still verifies cryptographically but the verification page flags Status: Revoked.
Request body
{
"reason": "Superseded by a re-issued version after grading correction.",
"reason_code": "reissued"
}reason_code— one of:reissued,issuer_error,recipient_request,other.reason— free-form, shown on the public verification page.
Response — 200 OK
{
"id": "crd_01HXYZ…",
"revoked": true,
"revoked_at": "2026-04-23T13:45:00Z",
"reason": "Superseded by a re-issued version after grading correction.",
"reason_code": "reissued"
}POST /v1/credentials/{id}/erase
Honor a GDPR right-to-be-forgotten request from the recipient. Redacts recipient PII from the off-chain store; the on-chain anchor and cryptographic proof are untouched. See /docs/guides/gdpr-erasure.
Request body
{
"requester": "recipient",
"verified_at": "2026-04-23T13:30:00Z"
}Response — 200 OK
{
"id": "crd_01HXYZ…",
"erased": true,
"erased_at": "2026-04-23T13:46:00Z",
"verification_status_after_erasure": "verifiable"
}Tenants
GET /v1/tenants/me
Return the tenant the current API key belongs to.
Response — 200 OK
{
"id": "tnt_01HXYZ…",
"name": "Example University",
"did": "did:web:learncoin.me#tenant-01HXYZ",
"plan": "starter",
"environment": "test",
"credentials_issued_this_month": 142,
"credentials_limit_this_month": 2500
}PATCH /v1/tenants/me
Update tenant-level metadata. Only the fields present in the body are updated.
Request body (all fields optional)
{
"display_name": "Example University of Tallinn",
"awarding_body": {
"name": "Example University of Tallinn",
"url": "https://example.edu",
"logo_url": "https://example.edu/logo.png"
},
"verify_page_branding": {
"accent_color": "#8B2635",
"logo_url": "https://example.edu/credential-logo.png"
}
}Webhooks
See /docs/guides/webhooks for the full event catalog, signature verification, and retry semantics.
POST /v1/webhooks
Register an endpoint to receive events.
Request body
{
"url": "https://your-backend.example.edu/learncoin/webhook",
"events": ["batch.anchored", "credential.revoked"],
"description": "Production event receiver"
}Response — 201 Created
{
"id": "whk_01HXYZ…",
"url": "https://your-backend.example.edu/learncoin/webhook",
"events": ["batch.anchored", "credential.revoked"],
"signing_secret": "whsec_…",
"created_at": "2026-04-23T12:00:00Z",
"active": true
}signing_secret— shown once at creation. Store it securely; required for verifying event signatures.
GET /v1/webhooks
List all registered endpoints for the current tenant.
DELETE /v1/webhooks/{id}
Remove an endpoint. Already-delivered events are unaffected; future events are not delivered to this endpoint.
POST /v1/webhooks/{id}/test
Send a synthetic webhook.test event to the registered URL so you can validate signature verification without issuing a real credential.
Response — 200 OK
{
"delivered": true,
"status_code": 200,
"delivered_at": "2026-04-23T13:00:00Z"
}Conventions (reminders)
Idempotency
Send Idempotency-Key: <uuid> on any POST to safely retry. The first request's result is cached for 24 hours; identical-key retries return the original response. Different request bodies with the same key return 409 Conflict with error idempotency_key_reused.
Pagination
All list endpoints use cursor-based pagination:
GET /v1/batches?cursor=eyJsYXN0X2lkIjoi…&limit=50Follow next_cursor until has_more is false. Cursors are opaque and stable for 24 h.
Errors
All error responses share this shape:
{
"error": {
"code": "credential_not_found",
"message": "No credential with ID crd_01HXYZ… exists in tenant tnt_01HXYZ.",
"request_id": "req_01HXYZABCDEF"
}
}Full catalog: /docs/api/errors.