API Reference
TypeScript types
Canonical TypeScript types for the LearnCoin API — copy these into your codebase for end-to-end type safety.
Machine-readable spec: learncoin.me/openapi.yaml (OpenAPI 3.1).
You can generate these types automatically from the OpenAPI spec via tools like openapi-typescript or orval. What follows is a hand-maintained mirror for use cases where a codegen step isn't desirable.
Install-free usage
// learncoin-types.ts — drop into your own codebase, no npm install.
// Keep in sync with https://learncoin.me/openapi.yamlPrimitives
/** API key. Prefix determines environment. */
export type ApiKey = `lrn_test_${string}` | `lrn_live_${string}`;
/** ULID-shaped resource identifiers. */
export type BatchId = `bat_${string}`;
export type CredentialId = `crd_${string}`;
export type TenantId = `tnt_${string}`;
export type WebhookId = `whk_${string}`;
export type EventId = `evt_${string}`;
/** Pseudonymous recipient ID — always a v4 UUID in urn form. */
export type RecipientId = `urn:uuid:${string}`;
/** ISO 8601 UTC timestamp. */
export type IsoDateTime = string;
/** Environment tag inferred from the API key prefix. */
export type Environment = "test" | "live";
/** Chain identifier for anchor transactions. */
export type Chain = "base-mainnet" | "base-sepolia";Request shapes
/** POST /v1/batches request body. */
export interface CreateBatchRequest {
credentials: CredentialInput[];
}
export interface CredentialInput {
recipient: RecipientInput;
achievement: AchievementInput;
issuanceDate: IsoDateTime;
expirationDate?: IsoDateTime;
}
export interface RecipientInput {
/** urn:uuid:<v4> — pseudonymous. Never put PII here. */
id: RecipientId;
/** Display name — stored off-chain, redactable on GDPR erasure. */
name: string;
/** Optional. Off-chain only. Used for magic-link delivery if enabled. */
email?: string;
}
export interface AchievementInput {
/** Stable URL identifying the achievement. Doesn't need to resolve. */
id?: string;
name: string;
description: string;
criteria?: { narrative?: string };
alignment?: AlignmentInput[];
evidence?: Array<{ id?: string; narrative?: string }>;
}
export interface AlignmentInput {
targetFramework: "ESCO" | "O*NET" | "ISCED-F" | "CUSTOM";
targetCode: string;
targetName: string;
targetUrl?: string;
}
/** POST /v1/credentials/{id}/revoke */
export interface RevocationRequest {
reason: string;
reason_code: "reissued" | "issuer_error" | "recipient_request" | "other";
}
/** POST /v1/credentials/{id}/erase */
export interface ErasureRequest {
requester: "recipient" | "dpo" | "supervisory_authority";
verified_at: IsoDateTime;
}
/** POST /v1/webhooks */
export interface CreateWebhookRequest {
url: string;
events: WebhookEventType[];
description?: string;
}Response shapes
/** POST /v1/batches response (202) + GET /v1/batches/{id} response (200). */
export interface Batch {
id: BatchId;
status: "pending" | "signed" | "anchored" | "failed";
credentials_count: number;
created_at: IsoDateTime;
anchored_at?: IsoDateTime | null;
environment: Environment;
merkle_root?: string | null;
anchor_transaction?: AnchorTransaction | null;
credentials?: Array<{
id: CredentialId;
recipient_id: RecipientId;
verify_url: string;
}>;
error?: string | null;
}
export interface AnchorTransaction {
chain: Chain;
hash: string;
block_number?: number;
explorer_url?: string;
}
/** GET /v1/credentials/{id} */
export interface Credential {
id: CredentialId;
verify_url: string;
status: "pending" | "signed" | "anchored";
revoked: boolean;
erased: boolean;
signed_credential: SignedCredentialJsonLd;
}
/** The signed JSON-LD artifact — triple-conformant (VC 2.0 + Blockcerts v3 + OB 3.0). */
export interface SignedCredentialJsonLd {
"@context": string[];
id: string;
type: string[];
issuer: {
id: string;
type?: string[];
name?: string;
};
issuanceDate: IsoDateTime;
expirationDate?: IsoDateTime | null;
credentialSubject: CredentialSubject;
proof: MerkleProof2019;
}
export interface CredentialSubject {
/** Always a urn:uuid:<v4>. */
id: RecipientId;
type: string[];
name?: string;
achievement: Achievement;
}
export interface Achievement {
id?: string;
type: string[];
name: string;
description: string;
criteria?: { narrative?: string };
alignment?: Alignment[];
}
export interface Alignment {
type: string[];
targetFramework: string;
targetCode: string;
targetName: string;
targetUrl?: string;
}
export interface MerkleProof2019 {
type: "MerkleProof2019";
created: IsoDateTime;
proofPurpose: "assertionMethod";
verificationMethod: string;
proofValue: string;
}
/** GET /v1/tenants/me + PATCH /v1/tenants/me */
export interface Tenant {
id: TenantId;
name: string;
did: string;
plan: "developer" | "starter" | "institution" | "industry";
environment: Environment;
credentials_issued_this_month?: number;
credentials_limit_this_month?: number;
}
/** POST /v1/webhooks response + GET /v1/webhooks items. */
export interface WebhookEndpoint {
id: WebhookId;
url: string;
events: WebhookEventType[];
/** Returned ONLY at creation. Save it immediately. */
signing_secret?: string;
description?: string;
active: boolean;
created_at: IsoDateTime;
}Webhook events
export type WebhookEventType =
| "batch.created"
| "batch.signed"
| "batch.anchored"
| "batch.failed"
| "credential.revoked"
| "credential.erased"
| "webhook.test";
export interface WebhookEventEnvelope<T extends WebhookEventType, D> {
id: EventId;
type: T;
created_at: IsoDateTime;
tenant_id: TenantId;
data: D;
}
export type WebhookEvent =
| WebhookEventEnvelope<
"batch.created",
{ batch_id: BatchId; credentials_count: number; environment: Environment }
>
| WebhookEventEnvelope<
"batch.signed",
{ batch_id: BatchId; merkle_root: string; signed_at: IsoDateTime }
>
| WebhookEventEnvelope<
"batch.anchored",
{
batch_id: BatchId;
merkle_root: string;
anchor_transaction: AnchorTransaction;
anchored_at: IsoDateTime;
credentials: Array<{
id: CredentialId;
recipient_id: RecipientId;
verify_url: string;
}>;
}
>
| WebhookEventEnvelope<
"batch.failed",
{
batch_id: BatchId;
error_code: string;
error_message: string;
failed_at: IsoDateTime;
}
>
| WebhookEventEnvelope<
"credential.revoked",
{
credential_id: CredentialId;
batch_id: BatchId;
revoked_at: IsoDateTime;
reason: string;
reason_code: RevocationRequest["reason_code"];
}
>
| WebhookEventEnvelope<
"credential.erased",
{
credential_id: CredentialId;
erased_at: IsoDateTime;
verification_status_after_erasure: "verifiable" | "invalid";
}
>
| WebhookEventEnvelope<
"webhook.test",
{ sent_at: IsoDateTime; note: string }
>;Errors
export interface ApiError {
error: {
code: ApiErrorCode;
message: string;
request_id: string;
};
}
export type ApiErrorCode =
// 401 — authentication
| "api_key_missing"
| "api_key_invalid"
| "api_key_revoked"
| "ip_not_allowed"
// 403 — authorization
| "environment_mismatch"
| "tier_required"
| "credential_not_owned"
// 400 — validation
| "invalid_json"
| "missing_required_field"
| "invalid_field_format"
| "recipient_id_invalid"
| "achievement_too_large"
| "batch_too_large"
| "alignment_framework_unknown"
// 404 — not found
| "batch_not_found"
| "credential_not_found"
| "webhook_not_found"
| "tenant_not_found"
// 409 — conflict
| "idempotency_key_reused"
| "credential_already_revoked"
| "credential_already_erased"
// 429 — rate limit
| "rate_limited"
// 422 — issuance
| "monthly_quota_exceeded"
| "signing_key_unavailable"
| "anchoring_chain_unavailable"
// 500 — server
| "internal_error";Minimal typed client
import type {
Batch,
Credential,
CreateBatchRequest,
ApiError,
} from "./learncoin-types";
export class LearnCoinClient {
constructor(
private readonly apiKey: string,
private readonly baseUrl = "https://api.learncoin.me/v1",
) {}
private async request<T>(
method: string,
path: string,
body?: unknown,
idempotencyKey?: string,
): Promise<T> {
const headers: Record<string, string> = {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
};
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
const res = await fetch(`${this.baseUrl}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
const payload = (await res.json()) as T | ApiError;
if (!res.ok) throw new Error(`LearnCoin: ${(payload as ApiError).error.code}`);
return payload as T;
}
createBatch(input: CreateBatchRequest, idempotencyKey?: string) {
return this.request<Batch>("POST", "/batches", input, idempotencyKey);
}
getBatch(id: string) {
return this.request<Batch>("GET", `/batches/${id}`);
}
getCredential(id: string) {
return this.request<Credential>("GET", `/credentials/${id}`);
}
}See also
- OpenAPI spec — source of truth for request/response shapes
- /docs/api/endpoints — hand-written reference mirror
- /docs/api/errors — full error-code catalog
- /docs/guides/webhooks — event verification + handlers