Live on Base with Ewance

See the certificates
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.yaml

Primitives

/** 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

On this page