VC Platform API (0.2.0)

Download OpenAPI specification:

License: Proprietary

Account/organization management, API key issuance, and multi-format credential issuance/verification for the VC Platform rebuild (see /requirements/PRD.md §5.1-§5.8). Covers everything implemented through Phase 2: org/membership CRUD, API keys, credential templates, four credential formats (VC-JWT, W3C VC 2.0 as JWT, W3C VC 1.1 with JSON-LD Data Integrity proofs, SD-JWT VC) over both the direct REST API and OpenID4VCI/VP 1.0 Final, Bitstring Status List revocation, and the queryable audit trail. Billing and webhooks are scaffolded but not yet built (see /requirements/ROADMAP.md).

Organizations

Org creation and the caller's org list (PRD §5.1)

Create an organization

Creates the org and adds the caller as its first admin member, in one transaction. No RLS exemption needed: the org id is generated app-side and the tenant context is set to it before the insert.

Authorizations:
DevUserAuth
Request Body schema: application/json
required
name
required
string [ 1 .. 200 ] characters
slug
required
string^[a-z0-9][a-z0-9-]{1,62}$

Lowercase alphanumeric with hyphens, 2-63 chars, starting alphanumeric.

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "slug": "string"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "string",
  • "slug": "string",
  • "createdAt": "2019-08-24T14:15:22Z",
  • "updatedAt": "2019-08-24T14:15:22Z"
}

List the caller's organizations

Pre-tenant lookup via the list_user_memberships() database function — runs before any org context exists.

Authorizations:
DevUserAuth

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Members

Org membership management, admin-gated except listing (PRD §5.1)

List an organization's members

Any member may list. A non-member gets the same 403 as a nonexistent org id — the membership check doubles as the tenant-existence check.

Authorizations:
DevUserAuth
path Parameters
orgId
required
string <uuid>

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Add a member

Admin-only. Upserts the invited user by email, then adds the membership.

Authorizations:
DevUserAuth
path Parameters
orgId
required
string <uuid>
Request Body schema: application/json
required
email
required
string <email>
role
required
string (OrgRole)
Enum: "admin" "developer" "auditor" "billing"

Org roles per PRD §5.1.

displayName
string [ 1 .. 200 ] characters

Responses

Request samples

Content type
application/json
{
  • "email": "user@example.com",
  • "role": "admin",
  • "displayName": "string"
}

Response samples

Content type
application/json
{
  • "userId": "2c4a230c-5085-4924-a3e1-25fb4fc5965b",
  • "email": "user@example.com",
  • "role": "admin"
}

Change a member's role

Admin-only. Refuses to demote the last remaining admin.

Authorizations:
DevUserAuth
path Parameters
orgId
required
string <uuid>
userId
required
string <uuid>
Request Body schema: application/json
required
role
required
string (OrgRole)
Enum: "admin" "developer" "auditor" "billing"

Org roles per PRD §5.1.

Responses

Request samples

Content type
application/json
{
  • "role": "admin"
}

Response samples

Content type
application/json
{
  • "orgId": "25b2c2d5-a7fc-47d0-89e4-8709a1560bfa",
  • "userId": "2c4a230c-5085-4924-a3e1-25fb4fc5965b",
  • "role": "admin",
  • "createdAt": "2019-08-24T14:15:22Z"
}

Remove a member

Admin-only. Refuses to remove the last remaining admin.

Authorizations:
DevUserAuth
path Parameters
orgId
required
string <uuid>
userId
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "statusCode": 0,
  • "error": "string",
  • "message": "string"
}

API Keys

Org-scoped API key issuance, listing, and revocation (PRD §5.2)

Issue an API key

Admin-only. The plaintext secret is returned exactly once in this response — only its SHA-256 hash is persisted. There is no way to retrieve it again after this call.

Authorizations:
DevUserAuth
path Parameters
orgId
required
string <uuid>
Request Body schema: application/json
required
name
required
string [ 1 .. 200 ] characters
scopes
required
Array of strings (ApiKeyScope) non-empty
Items Enum: "issue" "verify" "admin"
expiresAt
string <date-time>

ISO 8601 timestamp. Omit for a key that doesn't expire.

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "scopes": [
    ],
  • "expiresAt": "2019-08-24T14:15:22Z"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "name": "string",
  • "keyPrefix": "string",
  • "scopes": [
    ],
  • "createdBy": "25a02396-1048-48f9-bf93-102d2fb7895e",
  • "createdAt": "2019-08-24T14:15:22Z",
  • "expiresAt": "2019-08-24T14:15:22Z",
  • "lastUsedAt": "2019-08-24T14:15:22Z",
  • "revokedAt": "2019-08-24T14:15:22Z",
  • "secret": "string"
}

List API keys

Any member may list. Never includes the secret or its hash.

Authorizations:
DevUserAuth
path Parameters
orgId
required
string <uuid>

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Revoke an API key

Admin-only. Idempotent — revoking an already-revoked key returns 204 without recording a second audit event.

Authorizations:
DevUserAuth
path Parameters
orgId
required
string <uuid>
keyId
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "statusCode": 0,
  • "error": "string",
  • "message": "string"
}

Templates

Credential template & schema management, console-managed (PRD §5.3)

Create a credential template

Admin-only. claimsSchema is stored but not yet enforced against subjectClaims at issuance time. formats declares which wire formats this template may be rendered in — declaring an unimplemented format (e.g. mso_mdoc) is allowed as intent, but requesting it at issuance fails with 400, not at creation time. The same applies to statusMechanism: bitstring-status-list is implemented, token-status-list is declarable but rejected at issuance.

Authorizations:
DevUserAuth
path Parameters
orgId
required
string <uuid>
Request Body schema: application/json
required
name
required
string [ 1 .. 200 ] characters
claimsSchema
required
object

A JSON Schema the eventual subjectClaims are meant to satisfy — accepted and stored, not yet validated against at issuance.

formats
required
Array of strings (CredentialFormat) non-empty
Items Enum: "vc+jwt" "vc11+ld" "vc20" "dc+sd-jwt" "mso_mdoc"
validityPeriodDays
integer >= 1

If set, an issued credential's validUntil is validFrom + this many days. Omit for a credential that doesn't expire.

statusMechanism
string
Enum: "bitstring-status-list" "token-status-list"

Omit for irrevocable (expiry-only) credentials. When set, issuance allocates a status entry and embeds credentialStatus in the rendered credential.

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "claimsSchema": { },
  • "formats": [
    ],
  • "validityPeriodDays": 1,
  • "statusMechanism": "bitstring-status-list"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "orgId": "25b2c2d5-a7fc-47d0-89e4-8709a1560bfa",
  • "name": "string",
  • "claimsSchema": { },
  • "formats": [
    ],
  • "validityPeriodDays": 0,
  • "statusMechanism": "bitstring-status-list",
  • "createdAt": "2019-08-24T14:15:22Z",
  • "updatedAt": "2019-08-24T14:15:22Z"
}

List an organization's credential templates

Any member may list.

Authorizations:
DevUserAuth
path Parameters
orgId
required
string <uuid>

Responses

Response samples

Content type
application/json
[
  • {
    }
]

Get a credential template

Any member may read. A nonexistent or foreign-org template id gets 404 — same no-leak pattern as everywhere else org-scoped.

Authorizations:
DevUserAuth
path Parameters
orgId
required
string <uuid>
templateId
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "orgId": "25b2c2d5-a7fc-47d0-89e4-8709a1560bfa",
  • "name": "string",
  • "claimsSchema": { },
  • "formats": [
    ],
  • "validityPeriodDays": 0,
  • "statusMechanism": "bitstring-status-list",
  • "createdAt": "2019-08-24T14:15:22Z",
  • "updatedAt": "2019-08-24T14:15:22Z"
}

Credentials

Issuance and verification, API-key-authenticated (PRD §5.4, §5.5)

Issue a credential

API-key-authenticated (issue scope), not DevUserAuth — there is deliberately no {orgId} in this path; the API key itself is the tenant selector, the same way a Stripe key implies your account.

The org's Ed25519 signing key is provisioned lazily on first issuance (one key per org for Phase 0) and reused after. The platform constructs and encodes the JWS header/payload itself and sends only opaque signing-input bytes to the isolated signing service — it never sees credential content, and the platform never sees the private key (Architecture §5).

Authorizations:
ApiKeyAuth
Request Body schema: application/json
required
templateId
required
string <uuid>
subjectClaims
required
object

Not validated against the template's claimsSchema yet (Phase 0).

format
string
Enum: "vc+jwt" "vc11+ld" "vc20" "dc+sd-jwt" "mso_mdoc"

Which of the template's declared formats to render. Defaults to vc+jwt (the Phase 0 format) when omitted, so pre-Phase 1 callers are unaffected. Implemented: vc+jwt, vc20, vc11+ld, dc+sd-jwt (SD-JWT VC — every subject claim selectively disclosable, registered claims in the clear).

Responses

Request samples

Content type
application/json
{
  • "templateId": "196100ac-4eec-4fb6-a7f7-86c8b584771d",
  • "subjectClaims": { },
  • "format": "vc+jwt"
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "format": "vc+jwt",
  • "credential": "string",
  • "issuedAt": "2019-08-24T14:15:22Z",
  • "validFrom": "2019-08-24T14:15:22Z",
  • "validUntil": "2019-08-24T14:15:22Z"
}

Verify a credential

API-key-authenticated (verify scope). Signature verification never calls the signing service — the issuer identifier is a did:jwk, which embeds the public key directly, so resolving it is pure decoding, not a network round trip. Always returns 201 with a verified boolean and an errors array, whether the credential is valid or not — a malformed or invalid credential is a successful API call that reports failure, not an HTTP error. Audited either way (PRD §5.8's "pass/fail + reason").

Credentials carrying a credentialStatus entry additionally get a revocation check after signature verification, and it fails closed: a flipped bit yields revoked, and a status list that cannot be fetched or signature-verified yields status_unavailable — both are verification failures, since "could not determine revocation state" must not read as "valid". The platform's own lists are read locally; third-party lists are fetched over HTTP and signature-verified before a single bit is trusted.

Authorizations:
ApiKeyAuth
Request Body schema: application/json
required
credential
required
string non-empty

The wire credential to verify — compact JWT (vc+jwt / vc20), compact SD-JWT VC presentation, or a JSON-serialized vc11+ld credential document. The format is auto-detected: a leading { means JSON-LD, otherwise the JOSE typ header decides (falling back to the ~ separator), so there is no format field to pass. SD-JWT VC presentations may carry a key-binding JWT; it is verified against the credential's cnf key when present.

Responses

Request samples

Content type
application/json
{
  • "credential": "string"
}

Response samples

Content type
application/json
{
  • "verified": true,
  • "format": "vc+jwt",
  • "errors": [
    ],
  • "credential": {
    }
}

Revocation

Bitstring Status List v1.0 revocation (PRD §5.6, Architecture §4). Templates that declare statusMechanism: bitstring-status-list get a status entry allocated at issuance and a credentialStatus object embedded in the rendered credential (vc+jwt and vc20 formats). Revocation flips the credential's bit; verification then reports revoked.

The status list itself is served publicly at the host root (not under /api), because its URL is baked verbatim into issued credentials as statusListCredential and any relying party must be able to fetch it:

  • GET /status-lists/{orgId}/{listId} — the signed status list credential, a W3C VC 2.0 secured as a JWT (verifiable exactly like any vc20 credential this platform issues). Reveals only bit positions — never which credential or subject an index belongs to.

Revoke a credential

API-key-authenticated (issue scope — the key that may create an org's credentials is the one that may end them; a verify-only key can do neither). Flips the credential's bit on its status list and stamps revokedAt, in one transaction, audited as credential.revoked. Idempotent: revoking an already-revoked credential returns the original revocation without a second audit event. Only credentials whose template declared a statusMechanism can be revoked — anything else can only expire.

Authorizations:
ApiKeyAuth
path Parameters
credentialId
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "revoked": true,
  • "revokedAt": "2019-08-24T14:15:22Z"
}

Audit

Read side of the append-only audit trail (PRD §5.8) — admin and auditor roles

Query the audit trail

Readable by admin and auditor members — the auditor role is the PRD §4 compliance persona, and this endpoint is what it exists for. Newest-first, offset-paginated; hasMore is computed by fetching one extra row, not by counting an append-only table. Rows are immutable at the database layer (the app role has no UPDATE/DELETE grants on audit_events).

Authorizations:
DevUserAuth
path Parameters
orgId
required
string <uuid>
query Parameters
action
string

Exact match on the dotted action, e.g. credential.issued.

resourceType
string

e.g. credential, api_key, org, credential_template.

from
string <date-time>

Inclusive lower bound on the event timestamp.

to
string <date-time>

Exclusive upper bound on the event timestamp.

limit
integer [ 1 .. 200 ]
Default: 50
offset
integer >= 0
Default: 0

Responses

Response samples

Content type
application/json
{
  • "events": [
    ],
  • "limit": 0,
  • "offset": 0,
  • "hasMore": true
}

OpenID4VCI

OpenID4VCI 1.0 Final issuance (PRD §5.4, Architecture §4). Only offer creation lives under /api — the wallet-facing protocol endpoints sit at the host root, because every org is its own credential issuer at {publicUrl}/oid4vci/{orgId}:

  • GET /.well-known/openid-credential-issuer/oid4vci/{orgId} — issuer metadata
  • GET /.well-known/oauth-authorization-server/oid4vci/{orgId} — AS metadata
  • GET /oid4vci/{orgId}/jwks — org public key set (access-token verification)
  • POST /oid4vci/{orgId}/token — pre-authorized code → access token
  • POST /oid4vci/{orgId}/nonce — c_nonce endpoint (Final's dedicated nonce endpoint)
  • POST /oid4vci/{orgId}/credential — proof-of-possession → SD-JWT VC

Wire formats for those are defined by the OpenID4VCI / OAuth2 specs, not re-documented here; conforming wallets discover everything from the credential offer URI returned by POST /credentials/offers.

Create a credential offer

Starts the OpenID4VCI pre-authorized code flow: the org's backend (API key, issue scope) states what to issue; the response's credentialOffer URI goes to the holder as a QR code / deep link, and their wallet drives the rest against the wallet-facing endpoints (see the OpenID4VCI tag). The offer expires after expiresIn seconds, the pre-authorized code is single-use, and the credential is bound to the wallet's proof-of-possession key (cnf) at the credential endpoint — subject claims are fixed here, at offer time.

Authorizations:
ApiKeyAuth
Request Body schema: application/json
required
templateId
required
string <uuid>
subjectClaims
required
object

Fixed at offer time — the wallet receives exactly these claims.

format
string
Enum: "vc+jwt" "vc11+ld" "vc20" "dc+sd-jwt" "mso_mdoc"

Defaults to dc+sd-jwt, the only OpenID4VCI-offerable format so far.

Responses

Request samples

Content type
application/json
{
  • "templateId": "196100ac-4eec-4fb6-a7f7-86c8b584771d",
  • "subjectClaims": { },
  • "format": "vc+jwt"
}

Response samples

Content type
application/json
{
  • "credentialOffer": "string",
  • "credentialOfferObject": { },
  • "expiresIn": 0
}

OpenID4VP

OpenID4VP 1.0 Final relying-party flow with DCQL (PRD §5.5, Architecture §4). The org's backend creates a presentation request (unsigned, by-value, redirect_uri client-id prefix), shows the returned openid4vp:// URI to the holder, and polls the request for the outcome. The wallet answers by direct_post to the wallet-facing endpoint at the host root:

  • POST /oid4vp/{orgId}/response/{requestId} — authorization response (vp_token with DCQL-keyed SD-JWT VC presentations)

Presentations must carry a key-binding JWT: its signature is checked against the credential's cnf key, and its nonce/aud must match this exact request — a presentation captured from one exchange cannot be replayed into another.

Create a presentation request

Starts an OpenID4VP exchange (API key, verify scope): names the credential type being asked for (by template) and optionally which claims the wallet should disclose. Poll the returned id for the outcome; the request expires after expiresIn seconds.

Authorizations:
ApiKeyAuth
Request Body schema: application/json
required
templateId
required
string <uuid>

Template whose credential type (vct = template name) is being asked for.

requestedClaims
Array of strings

Claim names the wallet is asked to disclose. Omitted = the wallet decides.

Responses

Request samples

Content type
application/json
{
  • "templateId": "196100ac-4eec-4fb6-a7f7-86c8b584771d",
  • "requestedClaims": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "authorizationRequest": "string",
  • "expiresIn": 0
}

Poll a presentation request

Authorizations:
ApiKeyAuth
path Parameters
requestId
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
  • "status": "pending",
  • "vct": "string",
  • "templateId": "196100ac-4eec-4fb6-a7f7-86c8b584771d",
  • "result": {
    }
}