Live on Base with Ewance

See the certificates
API Reference

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

FieldTypeNotes
credentials[].recipient.idstringurn:uuid:… — pseudonymous recipient ID.
credentials[].recipient.namestringDisplay name; stored off-chain, redactable on erasure.
credentials[].achievement.namestringTitle of the achievement.
credentials[].achievement.descriptionstringBrief description.
credentials[].issuanceDatestringISO 8601 UTC.

Optional fields

FieldTypeNotes
credentials[].recipient.emailstringOff-chain only. Used for magic-link delivery if enabled.
credentials[].achievement.idstringStable URL identifying the achievement definition.
credentials[].achievement.criteria.narrativestringFree-form criteria prose.
credentials[].achievement.alignment[]arrayOB 3.0 alignment entries (ESCO, O*NET, custom frameworks).
credentials[].expirationDatestringISO 8601 UTC. Omit for non-expiring credentials.
credentials[].evidence[]arrayOptional 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"
}
  • status transitions: pendingsignedanchored (typical path, ≤ 60 s on testnet; ≤ 3 min on mainnet). Or: pendingfailed with error populated.
  • Subscribe to batch.anchored webhooks 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

ParamTypeNotes
cursorstringPagination cursor from a prior response.
limitinteger1–100, default 25.
statusstringFilter: 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=50

Follow 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.

On this page