API reference

Version 1.0. Base URL https://api.credda.io. The machine-readable contract is at /openapi.json.

Events

Append-only outcome ingestion (the only score input).

post /api/v1/events public

Ingest an outcome event

Appends an immutable event and asynchronously recomputes the score. Idempotent via `Idempotency-Key`.

Parameters

Idempotency-Key (header) string: Opaque, printable-ASCII string (8–255 chars). Makes the request safe to retry: the same key + payload replays the stored response (`Idempotent-Replay: true`) instead of re-executing. See /docs → Idempotency.

Request body

application/jsonEventInput

Responses

201Event recorded · object
400Validation error · Error
401Missing/invalid API key · Error
409Idempotency-Key still processing · Error
422Idempotency-Key reused with a different payload · Error

Example

curl -X POST https://api.credda.io/api/v1/events \
  -H "Content-Type: application/json" \
  -d '{ ... }'
post /api/v1/events/batch public

Ingest a batch of outcome events

Reports up to 100 events in one call, for a platform streaming many users' activity. Partial success: each item is independent, so a bad or duplicate item never fails the rest. Give each item an `idempotencyKey` to make a retried batch exactly-once.

Request body

application/jsonobject

Responses

200Batch processed (partial success) · object
400Validation error (0 or >100 events) · Error
401Missing/invalid API key · Error

Example

curl -X POST https://api.credda.io/api/v1/events/batch \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/events/export public

Export your own reported events (data portability)

Returns the events the authenticated platform itself reported, oldest-first (ledger order). `from`/`to` (ISO timestamps or dates, optional) filter on the event's recorded `createdAt`. Default `format=json` is cursor-paginated (`limit` 1–1000, default 100). `format=csv` streams a flat file, paginated server-side up to 50000 rows (`EVENT_EXPORT_MAX_ROWS`; advertised in the `X-Export-Max-Rows` response header); when the cap is hit the file ends with an explicit `# TRUNCATED` comment line, never a silent cut. Fields are the event's own recorded columns (user referenced by YOUR externalId); `autoImported` is surfaced from metadata. Read-only: exporting can never touch the ledger or a score. Requires `events:read` (or coarse `read`).

Parameters

from (query) string: Inclusive lower bound on the event `createdAt` (ISO timestamp or YYYY-MM-DD).
to (query) string: Inclusive upper bound on the event `createdAt` (ISO timestamp or YYYY-MM-DD).
format (query) json | csv: 'json' (default, cursor-paginated) or 'csv' (streamed, row-capped with an explicit truncation marker).
cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.
limit (query) integer: JSON page size (1–1000, default 100). Ignored for CSV.

Responses

200The platform's own events (JSON page by default; CSV stream when format=csv) · object
400Invalid query (EXPORT_FORMAT_INVALID / EXPORT_RANGE_INVALID) · Error
401Missing/invalid API key · Error
403API key lacks events:read · Error

Example

curl https://api.credda.io/api/v1/events/export

Scores

Read the deterministic score and its explanation.

get /api/v1/users public

List YOUR OWN subjects (query + export your book of business)

Lists the subjects the **calling platform itself** has reported at least one event for (your book of business), each with its current score + band, verification depth, your event counts, last-activity and subject type. This is the query surface a data platform runs its operation on, as opposed to a single-id lookup or a caller-supplied id list (`POST /users/scores`). **Strictly your own subjects.** A `User` row is global (the same person can be a counterparty of many platforms), so "your book" is defined by YOUR ledger: results are scoped, unconditionally, to subjects you have events for; you can never see, or guess your way to, another platform's subjects. Test/live isolation is enforced the same way (a test key lists only its test universe, a live key only live data; test-namespace prefixes are stripped from responses). **Filtering** is a closed, validated set: deliberately no free-form query language and no GraphQL, because the closed set is exactly what lets the tenant scope be guaranteed for every combination: `scoreMin`/`scoreMax`, `band`, `hasScore`, `scoreFrozen`, `subjectType`, `activeSince` (your events at/after an instant), `registeredSince`/`registeredBefore`, `hasVerifiedEvents`, `minVerifiedEvents`. **Sorting**: `sort` = `score` (default, desc) | `registered` | `externalId`, with `order`. (`lastActivity` is accepted for forward-compatibility; last-activity is exposed as a field and via `activeSince`; see note.) `eventCount`/`verifiedEventCount`/`lastActivityAt` are scoped to YOUR events, never a cross-platform total. **Subjects with no score yet** come back with `finalScore: null` and `scoreBand: null`, never a placeholder number. They are excluded from any `scoreMin`/`scoreMax`/`band` filter (a null satisfies no comparison); use `hasScore=false` to list exactly those. Default `format=json` is cursor-paginated (`limit` 1–1000, default 50). `format=csv` streams the whole book, paginated server-side up to 50000 rows (`BOOK_EXPORT_MAX_ROWS`; advertised in `X-Export-Max-Rows`) with an explicit `# TRUNCATED` trailer, never a silent cut. A CSV export is metered in PROPORTION to the rows returned (so it can't drain the book more cheaply than paging JSON), not as one flat call. Read-only under `scores:read`; nothing here computes or writes a score.

Parameters

scoreMin (query) number: Only subjects with `lastValidScore` ≥ this (0–100).
scoreMax (query) number: Only subjects with `lastValidScore` ≤ this (0–100).
band (query) Excellent | Good | Fair | Building | Unproven | High Risk: Only subjects in this reliability band (equivalent to the band's score range).
hasScore (query) boolean: true = only subjects whose score has been computed; false = only those still awaiting a first score. Cannot be combined with `scoreMin`/`scoreMax`/`band` (contradictory: 400).
scoreFrozen (query) boolean: true = only subjects whose score is frozen by a velocity flag; false = only unfrozen.
subjectType (query) PERSON | AGENT | ORGANIZATION: Filter to people, agents or organizations.
activeSince (query) string: Only subjects with ≥1 of YOUR events at/after this ISO instant.
registeredSince (query) string: Only subjects that first appeared in the ledger at/after this ISO instant (inclusive).
registeredBefore (query) string: Only subjects that first appeared in the ledger BEFORE this ISO instant (exclusive).
hasVerifiedEvents (query) boolean: true = only subjects with ≥1 of your VERIFIED events; false = only those with none.
minVerifiedEvents (query) integer: Only subjects with at least this many of YOUR verified events.
sort (query) score | lastActivity | registered | externalId: Sort key. Cursor-stable keys are score/registered/externalId; `lastActivity` is accepted for forward-compatibility.
order (query) asc | desc: Sort direction (defaults: desc for score/lastActivity, asc otherwise).
format (query) json | csv: 'json' (default, cursor-paginated) or 'csv' (whole-book export, row-capped with an explicit truncation marker).
cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.
limit (query) integer: JSON page size (1–1000, default 50). Ignored for CSV.

Responses

200A page of your subjects (JSON by default; CSV stream when format=csv) · object
400Invalid query (BOOK_QUERY_INVALID / BOOK_FORMAT_INVALID) · Error
401Missing/invalid API key · Error
403API key lacks scores:read · Error

Example

curl https://api.credda.io/api/v1/users
get /api/v1/users/summary public

Size and shape a segment of your book (counts, not rows)

Answers *how big is this segment, and what does it look like?* over the **same closed filter set** as `GET /api/v1/users`, so you can size a segment before paging or exporting it, and render "1,284 subjects, median 61.4, 18% Excellent" in one call instead of walking every page. Takes every filter `GET /api/v1/users` takes (`scoreMin`/`scoreMax`, `band`, `hasScore`, `scoreFrozen`, `subjectType`, `activeSince`, `registeredSince`/`registeredBefore`, `hasVerifiedEvents`, `minVerifiedEvents`) and returns counts + a band histogram + median/mean instead of rows. `sort`/`order`/`limit`/`cursor`/`format` are accepted and validated but have no meaning here. **Same isolation, same code path.** The summary is built from the identical tenant-scoped `where` the listing uses, so it can never count a subject the listing would not show you; test/live isolation rides along. **Nothing is faked.** `central.median`/`central.mean` are `null` when no subject in the segment is scored (a 0 there would read as a real, catastrophic score), and if the matched population exceeds the in-memory fold cap the exact `matched` count is still returned while the distribution comes back `null` with `aggregationSkipped` stating why; a partial aggregate is never presented as a whole-segment one. Read-only under `scores:read`; nothing here computes or writes a score. These are counts over your own records, never a verdict on any subject.

Parameters

scoreMin (query) number
scoreMax (query) number
band (query) Excellent | Good | Fair | Building | Unproven | High Risk
hasScore (query) boolean
scoreFrozen (query) boolean
subjectType (query) PERSON | AGENT | ORGANIZATION
activeSince (query) string
registeredSince (query) string
registeredBefore (query) string
hasVerifiedEvents (query) boolean
minVerifiedEvents (query) integer

Responses

200Counts and score shape for the matched segment · BookSummary
400Invalid query (BOOK_QUERY_INVALID / BOOK_FORMAT_INVALID) · Error
401Missing/invalid API key · Error
403API key lacks scores:read · Error

Example

curl https://api.credda.io/api/v1/users/summary
post /api/v1/users/scores public

Batch score read (up to 100 users in one call)

Request body

application/jsonobject

Responses

200Scores · object
400Invalid batch request · Error

Example

curl -X POST https://api.credda.io/api/v1/users/scores \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/users/{id}/score public

Current score

Parameters

id (path) required string

Responses

200Score · ScoreResult
404No score yet · Error

Example

curl https://api.credda.io/api/v1/users/{id}/score
get /api/v1/users/{id}/score/history public

Score snapshot history

Parameters

id (path) required string
cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.

Responses

200History · object

Example

curl https://api.credda.io/api/v1/users/{id}/score/history
get /api/v1/users/{id}/score/explain public

Plain-language score breakdown (with adverse-action reason codes)

A per-factor breakdown of the current score in plain language. Additively includes a `reasonCodes` object: deterministic, ranked ECOA / Regulation B **reason codes** for the record: `adverseActionReasons` (ranked most-significant-first, importance-weighted) and `supportingFactors`, each a stable code from `GET /api/v1/reason-codes` with a consumer-facing description and the specific evidence behind it. A B2B2C partner draws its statement of specific reasons from `adverseActionReasons` (top `keyFactorLimit`). Every code is reproducible from the ledger; `reasonCodesVersion` pins their meaning. **Credda supplies the attribution only; it takes no action and issues no notice; see `reasonCodes.disclosures`.**

Parameters

id (path) required string

Responses

200Explanation · object

Example

curl https://api.credda.io/api/v1/users/{id}/score/explain
get /api/v1/users/{id}/reliability public

Reliability at dispatch: the compact hot-path record read

The one small read a staffing/marketplace platform makes before assigning a shift: current score, band and confidence, whether the score is frozen, verified-evidence counts, `noShowRate` (the breach-type share of the outcome record), the engine's on-time component, days since the last event, and the top ranked drivers, under ~1KB. **Read-only.** Every value is either already stored on the latest `ScoreSnapshot` or a plain count over the append-only ledger. Nothing here computes or writes a score (`calculateCreddaScore` remains the sole snapshot writer) and a subject the engine has never scored reads `null` rather than triggering a computation. Absent data is always `null`, never a fake `0`. **Evidence, never a verdict.** No field says call / don't-call, fit / unfit, or approve / deny. Credda reports the record; the platform applies its own criteria and owns the dispatch decision. **If you use this read to SELECT workers, FCRA (or local equivalents) may attach to that decision; scope it with your counsel.** The `band` labels are the factual reliability bands the deterministic formula produces. `context` defaults to `dispatch`, the only context served today; an unrecognised value is a 400 (`DISPATCH_CONTEXT_INVALID`) rather than a silently different projection.

Parameters

id (path) required string
context (query) dispatch: Which compact projection to serve. Only `dispatch` today.

Responses

200Compact reliability read · object
400Unknown context (DISPATCH_CONTEXT_INVALID) · Error
404User not found · Error

Example

curl https://api.credda.io/api/v1/users/{id}/reliability
get /api/v1/users/{id}/trust-summary public

Evidence-based trust explanation (summary, strengths, risks)

Deterministic answer to "why is this score what it is": a one-sentence summary plus strengths and risks, every claim derived from ledger facts (completion/on-time rates, verified-evidence confidence, dispute record, platform diversity, recency, advisory risk signals). Deliberately contains NO verdict or recommendation: Credda explains evidence; it never decides hire/approve. `?narrative=1` additionally attaches an advisory AI retelling of the same facts when the AI subsystem is enabled (inert by default).

Parameters

id (path) required string
narrative (query) 1: Set to 1 to request the optional advisory AI narrative.

Responses

200Trust explanation · object
404User not found · Error

Example

curl https://api.credda.io/api/v1/users/{id}/trust-summary
get /api/v1/users/{id}/score/delta public

Factor-level explanation of the last score change

Parameters

id (path) required string

Responses

200Score delta · object
404User not found · Error

Example

curl https://api.credda.io/api/v1/users/{id}/score/delta
get /api/v1/users/{id}/score/components public

Modular score components (Reliability, Timeliness, Trustworthiness, Verification Confidence, Consistency, Momentum)

Reframes the score as named, independently 0–100-scored components instead of one number. Pure relabeling of the same data /score/explain exposes: no new formula, cannot regress finalScore.

Parameters

id (path) required string

Responses

200Score components · object
404User not found · Error

Example

curl https://api.credda.io/api/v1/users/{id}/score/components
get /api/v1/users/{id}/timeline public

Unified chronological feed of events + score changes

Merges the Event ledger and ScoreSnapshot history into one cursor-paginated, newest-first feed. Read-only: a view, not a new source of truth.

Parameters

id (path) required string
cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.

Responses

200Timeline · object
404User not found · Error

Example

curl https://api.credda.io/api/v1/users/{id}/timeline
post /api/v1/users/{id}/score/project public

What-if score projection (read-only, never writes a snapshot)

Layer 1–20 hypothetical events on the user's current state and get the resulting score, deterministically, without writing anything. If the user id does not exist yet, the projection runs from a blank, unproven baseline, so you can simulate a trust trajectory "from zero" (e.g. what 10 verified deliveries would produce) before reporting a single event. `eventType` accepts the FULL event vocabulary, not just the platform-ingestable subset, including `CONTRACT_BREACHED` (the strongest negative signal) and the dispute outcomes the API writes itself, so the worst case is modellable without writing anything.

Parameters

id (path) required string

Request body

application/jsonobject

Responses

200Projection · object
400Invalid projection request · Error
404User not found · Error

Example

curl -X POST https://api.credda.io/api/v1/users/{id}/score/project \
  -H "Content-Type: application/json" \
  -d '{ ... }'
post /api/v1/users/{id}/documents/analyze public

Résumé / document advisory: who confirms it, and how it affects the score (writes nothing)

Given the STRUCTURED claims a résumé or work-history document describes, advises (per claim): **who the third-party witness is** (the counterparty who can confirm it), **whether it counts as verified as submitted** (only if it already names a witness), and (read-only) **how adding the claims moves the score** in two scenarios: as-you'd-submit-now vs. if every claim is confirmed (via the read-only projection). **Writes nothing**: no event, no snapshot, no `isVerified`. A claim becomes verified evidence only when its named witness actually confirms it (a Credda confirmation or reference request), never by calling this. Renders no verdict on the person. The deterministic API takes STRUCTURED claims only; turning a raw document into structured claims is the advisory-AI onboarding flow's job. `scores` scope.

Parameters

id (path) required string

Request body

application/jsonobject

Responses

200Per-claim advice + a two-scenario score projection + witness guide · object
400Invalid request (provide 1–50 structured claims) · Error

Example

curl -X POST https://api.credda.io/api/v1/users/{id}/documents/analyze \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/users/{id}/platforms public

Contributing platforms

Parameters

id (path) required string

Responses

200Platforms · object

Example

curl https://api.credda.io/api/v1/users/{id}/platforms
get /api/v1/users/{id}/risk public

Advisory anti-gaming risk signals (never affects the score)

Parameters

id (path) required string

Responses

200Risk signals · object

Example

curl https://api.credda.io/api/v1/users/{id}/risk
get /api/v1/usage public

Your API usage vs. tier quota

Per-day request counts (by status class) for the authenticated platform, the current tier rate limit, and `endpoints`: the busiest route patterns over the window (top 10, descending). Window: either a trailing `days` window (default 7, max 400) OR an explicit inclusive `from`/`to` date range for monthly statements, mutually exclusive (400 `USAGE_WINDOW_CONFLICT` if both are given). Live counters are metered in Redis (90-day retention); completed days are rolled up durably, so statements reach back up to 400 days; a range beyond that is clamped (the response reports the effective window and `truncated: true`). Days with no data in either store report zeros. `format=csv` returns the same statement as a flat `text/csv` attachment (`row` column discriminates day / total / endpoint rows) named `credda-usage-<from>-<to>.csv`; default remains JSON.

Parameters

days (query) integer: Trailing window in days (default 7, max 400). Mutually exclusive with `from`/`to`.
from (query) string: Inclusive range start (ISO date, YYYY-MM-DD). Requires `to`; clamped to the 400-day history window (completed days beyond the 90-day live retention are served from durable daily rollups).
to (query) string: Inclusive range end (ISO date, YYYY-MM-DD). Requires `from`.
format (query) json | csv: Response format: 'json' (default) or 'csv' (flat statement, `Content-Disposition: attachment`).

Responses

200Usage summary (JSON by default; CSV statement when format=csv) · object
400Invalid window (USAGE_WINDOW_CONFLICT / USAGE_RANGE_INVALID / USAGE_FORMAT_INVALID) · Error

Example

curl https://api.credda.io/api/v1/usage
get /api/v1/usage/quota public

Quota transparency: remaining monthly calls + reset

The one cheap read to poll "how many calls do I have left this month, and when does it reset?" without pulling the full per-day usage breakdown. Read-only: it never counts against your quota, and mirrors the exact numbers the enforcement path uses, so a client can throttle itself before it ever sees a 429. `unlimited: true` (`cap: null`) means the tier has no monthly quota configured. `secondsUntilReset` matches the `Retry-After` header sent on a 429.

Responses

200Quota state · object

Example

curl https://api.credda.io/api/v1/usage/quota
get /api/v1/usage/meters public

Usage as metered-billing meters (Stripe Billing Meters shape)

The BILLING view of usage: per-metered-dimension request totals over the window, ready to push to a metered-billing system. A pure reprojection of the SAME usage counters `GET /usage` serves: `dimension: total` (the primary billable quantity, value `all`), `dimension: status_class` (ok / clientError / serverError), and `dimension: endpoint` (per route pattern, mirroring the `/usage` busiest-endpoint list and its top-N bound). Same window controls as `/usage` (`days` OR `from`/`to`, mutually exclusive, clamped to retention) and `format=json|csv` (RFC-4180 `Content-Disposition` attachment). Requires the `usage` scope. Read-only: Credda emits usage; the biller prices it; no score is read or written.

Parameters

days (query) integer: Trailing window in days (default 7, max 400). Mutually exclusive with `from`/`to`.
from (query) string: Inclusive range start (ISO date, YYYY-MM-DD). Requires `to`; clamped to the 400-day history window (completed days beyond the 90-day live retention are served from durable daily rollups).
to (query) string: Inclusive range end (ISO date, YYYY-MM-DD). Requires `from`.
format (query) json | csv: Response format: 'json' (default) or 'csv' (flat statement, `Content-Disposition: attachment`).

Responses

200Meter rows (JSON by default; CSV attachment when format=csv) · object
400Invalid window (USAGE_WINDOW_CONFLICT / USAGE_RANGE_INVALID / USAGE_FORMAT_INVALID) · Error

Example

curl https://api.credda.io/api/v1/usage/meters
get /api/v1/scoring/model public

The scoring model, as data

Weights, the unproven-user anchor, the confidence thresholds, the recency decay rate and the provisional-activity band (the score range unverified activity can reach; verification is the only path beyond it), all read directly from the formula constants rather than restated, so it cannot drift from the engine it describes. Public and unauthenticated: a caller deciding whether to trust a score should be able to read how it is computed, and check a score against the model themselves, without holding a key.

Responses

200Scoring model · object

Example

curl https://api.credda.io/api/v1/scoring/model

Analytics

Tenant-scoped time-series aggregates over the caller's OWN data: event volume by day and type with the verified share, and the caller's subjects' score band mix, central tendency and movement over a window. Aggregate-only, read-only (scores:read), test/live isolated; nothing here computes or writes a score, and the numbers are distribution facts, never a verdict on any subject.

get /api/v1/analytics/events public

Event volume time-series (by day + by type + verified & confirmed share)

Tenant-scoped aggregates over the CALLER's OWN event ledger: `daily` (one row per UTC day, gaps filled with zeros: total, verified, confirmed, verifiedShare, confirmedShare), `byType` (per event type, busiest first), and window `totals` with the overall shares. **`verified` vs `confirmed`:** `verified` counts the raw `isVerified` flag, which on a directly-reported event is your OWN assertion; `confirmed` counts only events written because a DISTINCT counterparty acted on a one-time confirmation token (and were not downgraded as self-attested). `confirmed` is always a subset of `verified`; it is the counterparty-confirmed evidence density of your integration. Window: a trailing `days` window (default 30, max `ANALYTICS_MAX_DAYS`=365) OR an explicit inclusive `from`/`to` range, mutually exclusive (400 `ANALYTICS_WINDOW_CONFLICT`). Aggregate-only: it returns no subject identifiers, only counts. Requires `scores:read`; test/live isolated. Read-only; nothing here computes or writes a score.

Parameters

days (query) integer: Trailing window in days (default 30, max 365). Mutually exclusive with `from`/`to`.
from (query) string: Inclusive range start (ISO date, YYYY-MM-DD). Requires `to`.
to (query) string: Inclusive range end (ISO date, YYYY-MM-DD). Requires `from`.

Responses

200Event analytics · object
400Invalid window (ANALYTICS_WINDOW_CONFLICT / ANALYTICS_RANGE_INVALID) · Error

Example

curl https://api.credda.io/api/v1/analytics/events
get /api/v1/analytics/scores public

Score distribution + movement across your subjects

Tenant-scoped aggregates over the CALLER's subjects (those you have reported at least one event for): `scoredSubjects` count, `central` (median + mean of current scores), `bandDistribution` (current-score band histogram, highest first, with share), and `movement`: how many scores rose / fell / stayed level between consecutive recomputations WITHIN the window, plus the number of subjects that moved or were recomputed. Same window controls as `/analytics/events`. Aggregate-only: no subject identifiers, only counts and distribution facts, never a verdict. Requires `scores:read`; test/live isolated. Read-only; `calculateCreddaScore` stays the sole snapshot writer.

Parameters

days (query) integer: Trailing window in days (default 30, max 365). Mutually exclusive with `from`/`to`.
from (query) string: Inclusive range start (ISO date, YYYY-MM-DD). Requires `to`.
to (query) string: Inclusive range end (ISO date, YYYY-MM-DD). Requires `from`.

Responses

200Score analytics · object
400Invalid window (ANALYTICS_WINDOW_CONFLICT / ANALYTICS_RANGE_INVALID) · Error

Example

curl https://api.credda.io/api/v1/analytics/scores

Webhooks

Subscribe to signed trust events (advisory, never affects a score).

get /api/v1/events/stream public

Real-time event stream (Server-Sent Events)

Holds one long-lived HTTP connection and streams the SAME event envelopes Credda POSTs to your webhooks (score.updated, score.band_changed, dispute.resolved, monitor.triggered, policy.threshold_crossed, usage.quota_warning, import.completed, screening.completed), in real time, so you can drop a webhook receiver or stop busy-polling. The response is `text/event-stream` (SSE): each event is an `id:` line (the event id; the browser echoes it as `Last-Event-ID` on reconnect), an `event:` line (the type), and a single-line JSON `data:` field carrying the delivery envelope `{id, type, livemode, createdAt, data}`, terminated by a blank line. A `:` comment heartbeat is sent periodically to keep the connection alive. Optional `?types=score.updated,monitor.triggered` restricts the stream to those event types (an unknown type is a 400, same as GET /webhooks/deliveries). Mode isolation: a test key (crd_test_) streams only `livemode:false` envelopes, a live key only live ones. Resume is best-effort: send `Last-Event-ID` (header, or `?lastEventId=`) to replay buffered events after that id (at-most-once past the short buffer window). Gated under the EXISTING `webhooks` scope: a stream is webhook-notification config, not a new scope resource. NOTIFICATION ONLY: this endpoint reads and writes NOTHING score-side.

Parameters

types (query) string: Comma-separated event types to include (default: all). An unknown type is a 400.
lastEventId (query) string: Resume cursor: replay buffered events after this id (best-effort). Prefer the `Last-Event-ID` header, which browsers set automatically on reconnect.
Last-Event-ID (header) string: Resume cursor set automatically by browser EventSource on reconnect. Same effect as `?lastEventId=`.

Responses

200An open Server-Sent Events stream. Content-Type is text/event-stream; the body is an unbounded sequence of SSE records, one per event, plus periodic `:` heartbeat comments. Not a JSON body.
400Unknown eventType in ?types · Error
401Missing/invalid API key · Error
403API key lacks the webhooks scope · Error

Example

curl https://api.credda.io/api/v1/events/stream
get /api/v1/webhooks public

List your webhooks

Responses

200Webhooks · object

Example

curl https://api.credda.io/api/v1/webhooks
post /api/v1/webhooks public

Subscribe an HTTPS endpoint (secret returned once)

Request body

application/jsonobject

Responses

201Created · object
400Invalid url/events · Error

Example

curl -X POST https://api.credda.io/api/v1/webhooks \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/webhooks/{id} public

Fetch one webhook

Parameters

id (path) required string

Responses

200Webhook · object
404Not found · Error

Example

curl https://api.credda.io/api/v1/webhooks/{id}
patch /api/v1/webhooks/{id} public

Update url/events/description/isActive

Parameters

id (path) required string

Request body

application/jsonobject

Responses

200Updated · object

Example

curl -X PATCH https://api.credda.io/api/v1/webhooks/{id} \
  -H "Content-Type: application/json" \
  -d '{ ... }'
delete /api/v1/webhooks/{id} public

Delete a webhook

Parameters

id (path) required string

Responses

204Deleted

Example

curl -X DELETE https://api.credda.io/api/v1/webhooks/{id}
post /api/v1/webhooks/{id}/test public

Send a synthetic signed delivery

Parameters

id (path) required string

Responses

200Delivery result · object

Example

curl -X POST https://api.credda.io/api/v1/webhooks/{id}/test
get /api/v1/webhooks/deliveries public

Recent outbound events across ALL your endpoints (sample data)

The "perform-list" source automation platforms (Zapier, Make, n8n) need to render sample data BEFORE a trigger has ever fired. Returns your most recent outbound events across every webhook endpoint you own, newest first, cursor-paginated; each item is the delivery envelope exactly as sent (`id`, `type`, `livemode`, `createdAt`, `data`), so a field mapping built against a sample keeps working verbatim on real deliveries. One item per EVENT, not per attempt (retry history lives at `/webhooks/{id}/deliveries`). If you have no retained deliveries yet, the representative payloads from `GET /api/v1/webhooks/events` are returned instead so sample data is never empty; those items carry `isExample: true` and a null `delivery`, and must never be shown as something that actually happened (`source` is `examples` rather than `deliveries`). Test-mode isolated: a `crd_test_` key sees only `livemode:false` events, a live key only live ones. Advisory: reading this never affects a score.

Parameters

cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.
limit (query) integer
eventType (query) score.updated | score.band_changed | dispute.resolved | monitor.triggered | policy.threshold_crossed | usage.quota_warning | import.completed | screening.completed | confirmation.awaiting_response | confirmation.expiring_soon: Filter to one or more event types (repeat the param or comma-separate). An unknown type is a 400, not an empty result.

Responses

200Recent events · object
400Unknown eventType (BAD_REQUEST) · Error

Example

curl https://api.credda.io/api/v1/webhooks/deliveries
get /api/v1/webhooks/{id}/deliveries public

Recent delivery attempts

Parameters

id (path) required string
cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.

Responses

200Deliveries · object

Example

curl https://api.credda.io/api/v1/webhooks/{id}/deliveries
post /api/v1/webhooks/{id}/deliveries/{deliveryId}/replay public

Replay a past delivery

Re-sends the stored event body, signed with the current secret and a fresh transport timestamp. The event keeps its ORIGINAL id: deliveries are at-least-once and consumers dedup on it, so a replay behaves like a duplicate delivery, not a new event. Single attempt; recorded in the delivery log. 409 PAYLOAD_NOT_RETAINED for deliveries recorded before payload retention.

Parameters

id (path) required string
deliveryId (path) required string

Responses

200Replay outcome · object
404Delivery not found · Error
409Payload not retained · Error

Example

curl -X POST https://api.credda.io/api/v1/webhooks/{id}/deliveries/{deliveryId}/replay
get /api/v1/webhooks/events public

Outbound webhook event catalog

Every event type the API can send (score.updated, score.band_changed, dispute.resolved, monitor.triggered, usage.quota_warning), with the common delivery envelope, an example payload for each, and how to verify a delivery signature. The envelope carries `livemode` (Stripe convention): `false` when the event was produced by test-mode (crd_test_) activity, `true` otherwise. Self-documenting and drift-proof against the delivery pipeline. Public and unauthenticated. INVARIANT: webhooks are advisory: no event, and no consumer, can change anyone's score.

Responses

200Webhook event catalog · object

Example

curl https://api.credda.io/api/v1/webhooks/events

Monitors

Continuous score monitoring: edge-triggered threshold/band watches that fire monitor.triggered webhook events. Notification config only; never affects a score.

get /api/v1/monitors public

List your score monitors

Parameters

cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.
limit (query) integer

Responses

200Monitors · object

Example

curl https://api.credda.io/api/v1/monitors
post /api/v1/monitors public

Create a score monitor on one of your users

Registers a continuous watch on a user's score. At least one condition is required: `belowScore` (fires on a downward crossing, and on a user's FIRST score when it is already below the threshold), `aboveScore` (fires on an upward crossing only; never on a first score), or `onBandChange` (fires when the band label changes; never on a first score). When a condition fires, a `monitor.triggered` event is delivered through your subscribed webhooks. Active monitors per platform are capped by plan tier (Starter 5 / Growth 250 / Enterprise 2000 by default, from the plan catalog, overridable via `MONITOR_LIMIT_<TIER>` env vars or the global `MONITOR_MAX_PER_PLATFORM` backstop). At the cap the 409 body's `details` carries `{limit, tier}` so callers can present an upgrade path. Monitors are notification config only and never affect a score.

Request body

application/jsonobject

Responses

201Created · object
400No condition provided / validation error · Error
404Unknown userId (USER_NOT_FOUND) · Error
409Active monitor limit reached for the plan tier (MONITOR_LIMIT_REACHED; `details` carries `{limit, tier}`) · Error

Example

curl -X POST https://api.credda.io/api/v1/monitors \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/monitors/{id} public

Fetch one monitor

Parameters

id (path) required string

Responses

200Monitor · object
404Not found · Error

Example

curl https://api.credda.io/api/v1/monitors/{id}
patch /api/v1/monitors/{id} public

Update thresholds / onBandChange / isActive

Set `belowScore`/`aboveScore` to null to clear a threshold. The updated monitor must keep at least one condition.

Parameters

id (path) required string

Request body

application/jsonobject

Responses

200Updated · object
400Update would leave the monitor with no condition · Error
404Not found · Error

Example

curl -X PATCH https://api.credda.io/api/v1/monitors/{id} \
  -H "Content-Type: application/json" \
  -d '{ ... }'
delete /api/v1/monitors/{id} public

Delete a monitor (hard delete; it is config, not ledger data)

Parameters

id (path) required string

Responses

204Deleted
404Not found · Error

Example

curl -X DELETE https://api.credda.io/api/v1/monitors/{id}

Notification Channels

Slack + email destinations that receive the SAME trust events as webhooks, for a partner who runs no webhook receiver. Delivery is fire-and-forget; notification config only, never affects a score.

get /api/v1/notification-channels public

List your notification channels

Parameters

cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.
limit (query) integer

Responses

200Notification channels · object

Example

curl https://api.credda.io/api/v1/notification-channels
post /api/v1/notification-channels public

Create a Slack or email notification channel

Registers a destination that receives the SAME trust events (score.updated, score.band_changed, monitor.triggered, policy.threshold_crossed, dispute.resolved, usage.quota_warning) your webhooks would, so a partner who runs no webhook receiver still gets notified. `type` is `slack` (a `target` https `hooks.slack.com/services/…` incoming-webhook URL; pinned to that host, SSRF-safe) or `email` (a `target` address). Slack posts a `{text}` carrying the event envelope; email delivery is inert until the deployment configures SMTP (the api ships no SMTP dependency), so an email channel is store-and-validate today. Channels are notification config under the existing `webhooks` scope and never affect a score. Test-mode keys manage/deliver to test channels only. Slack targets are masked in responses (the URL is a bearer secret).

Request body

application/jsonobject

Responses

201Created · object
400Invalid type/target (NOTIFICATION_CHANNEL_INVALID) · Error
409Per-platform channel limit reached (NOTIFICATION_CHANNEL_LIMIT_REACHED; `details` carries `{limit}`) · Error

Example

curl -X POST https://api.credda.io/api/v1/notification-channels \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/notification-channels/{id} public

Fetch one channel

Parameters

id (path) required string

Responses

200Notification channel · object
404Not found · Error

Example

curl https://api.credda.io/api/v1/notification-channels/{id}
delete /api/v1/notification-channels/{id} public

Delete a channel (hard delete; it is config, not ledger data)

Parameters

id (path) required string

Responses

204Deleted
404Not found · Error

Example

curl -X DELETE https://api.credda.io/api/v1/notification-channels/{id}

Policies

Threshold policies: declarative, partner-grade rules that fire a policy.threshold_crossed webhook when a subject (one user, or all your subjects) crosses a score/component/verified-event line or a band. Credda supplies evidence + a decision_input block; the partner owns the decision and the loss. Notification config only; never affects a score.

get /api/v1/policies public

List your threshold policies

Parameters

cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.
limit (query) integer

Responses

200Policies · object

Example

curl https://api.credda.io/api/v1/policies
post /api/v1/policies public

Create a threshold policy

Registers a declarative rule that fires a `policy.threshold_crossed` webhook when a subject crosses a line, edge-triggered. Scope is exactly one of `userId` (one subject) or `appliesToAll: true` (every one of your subjects; auto-covers new subjects, no per-subject registration). The condition is one `metric`: `score`/`component`/`verified_events` (a `direction` up|down + a `threshold`; `component` also names a component) or `band` (`direction` enter|leave + a `band`, or neither to watch any band change). Credda supplies EVIDENCE of the crossing + a machine-readable `decision_input` block for your OWN policy engine; the decision and the loss belong to you. Active policies per platform are capped by plan tier (Starter 5 / Growth 250 / Enterprise 2000 by default, budgeted separately from monitors, overridable via `POLICY_LIMIT_<TIER>` / `POLICY_MAX_PER_PLATFORM`). Policies are notification config only and never affect a score.

Request body

application/jsonobject

Responses

201Created · object
400Invalid scope or condition · Error
404Unknown userId (USER_NOT_FOUND) · Error
409Active policy limit reached for the plan tier (POLICY_LIMIT_REACHED; `details` carries `{limit, tier, mode}`) · Error

Example

curl -X POST https://api.credda.io/api/v1/policies \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/policies/{id} public

Fetch one policy

Parameters

id (path) required string

Responses

200Policy · object
404Not found · Error

Example

curl https://api.credda.io/api/v1/policies/{id}
patch /api/v1/policies/{id} public

Retune the line / rename / activate / deactivate

Update `name`, `isActive`, or the condition fields (`direction`/`threshold`/`component`/`band`). The `metric` is immutable; to change it, delete and recreate. The merged condition is re-validated with the same rules as create.

Parameters

id (path) required string

Request body

application/jsonobject

Responses

200Updated · object
400Update would leave an invalid condition · Error
404Not found · Error
409Active policy limit reached (POLICY_LIMIT_REACHED) · Error

Example

curl -X PATCH https://api.credda.io/api/v1/policies/{id} \
  -H "Content-Type: application/json" \
  -d '{ ... }'
delete /api/v1/policies/{id} public

Delete a policy (hard delete; it is config, not ledger data)

Parameters

id (path) required string

Responses

204Deleted
404Not found · Error

Example

curl -X DELETE https://api.credda.io/api/v1/policies/{id}

Confirmations

Counterparty confirmation requests: propose an outcome and have a DISTINCT party confirm it through a one-time token, so isVerified is EARNED by a third-party witness rather than asserted by the reporting platform. A pending request writes no event and moves no score; only a genuine confirmation writes to the ledger (through the same append-only path as POST /events). The confirm/preview endpoints are token-gated and unauthenticated: a counterparty holds a token, not an API key.

get /api/v1/confirmations public

List your confirmation requests

Parameters

cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.
limit (query) integer
status (query) PENDING | CONFIRMED | DECLINED | EXPIRED | CANCELLED: Filter to one lifecycle state.

Responses

200Confirmation requests · object

Example

curl https://api.credda.io/api/v1/confirmations
post /api/v1/confirmations public

Propose an outcome for a counterparty to confirm

Creates a PENDING confirmation request and returns a ONE-TIME token; it writes NO event and moves NO score. Deliver the link to the named counterparty over your OWN channel (Credda sends nothing to anyone): either the hosted page `confirmUrl` (zero frontend to build) or your own UI over `previewUrl`/`respondUrl`. Only when they confirm is the proposed event written to the ledger with isVerified: true. That verified flag is EARNED (a distinct party confirmed), never asserted, which is the whole difference from POST /events. `counterpartyRef` must differ from `userId` (a subject cannot be its own witness → 400 CONFIRMATION_SELF); it is recorded into the resulting event metadata so cross-account velocity/graph signals can see who confirmed. The event type is one of the ingestable outcomes (positive OR negative; a counterparty can confirm a breach as validly as a delivery). Send an `Idempotency-Key` header to make a retried create exactly-once. FREE AT EVERY TIER: any valid platform key may create requests, no scope required (the ask is never paywalled; only a genuine counterparty confirm records an event). INVARIANT: pending/declined/expired requests write nothing; only CONFIRMED reaches the ledger, through the same path as every other event.

Parameters

Idempotency-Key (header) string: Optional; makes a retried create return the original request + token instead of a duplicate.

Request body

application/jsonobject

Responses

201Created (token shown once) · object
400Validation error, or counterpartyRef equals the subject (CONFIRMATION_SELF) · Error

Example

curl -X POST https://api.credda.io/api/v1/confirmations \
  -H "Content-Type: application/json" \
  -d '{ ... }'
post /api/v1/confirmations/batch public

Bulk-create confirmation requests (the activation engine)

Turn a BOOK of historical relationships, such as past jobs, placements, engagements, projects, for a professional in ANY field (a nurse's shift, a lawyer's matter, a contractor's job, a designer's project), into up to 100 PENDING confirmation requests in one call, warming a cold ledger with real counterparty asks. Each item is exactly the single POST /confirmations body and flows through the SAME create service, so isVerified is still earned in exactly one place (on confirm), never here. INVARIANT: a batch item writes NOTHING to the ledger; importing a book creates pending asks, it does NOT manufacture verified events. Each event is written only when ITS named counterparty confirms. Partial success like POST /events/batch: a bad item (e.g. counterpartyRef equals the subject → CONFIRMATION_SELF) fails INDIVIDUALLY with its index + reason; the rest still land. Each ok item returns its one-time token + hosted confirmUrl to deliver over your OWN channel (Credda sends nothing). An over-cap batch is a 400 (rejected whole, never truncated). Send an `Idempotency-Key` header to make a retried batch exactly-once; there is no per-item key. FREE AT EVERY TIER: any valid platform key, no scope required (same as the single create).

Parameters

Idempotency-Key (header) string: Optional; makes a retried batch replay the original response instead of creating duplicates.

Request body

application/jsonobject

Responses

200Per-item results (partial success) · object
400Validation error, or over the 100-item cap · Error

Example

curl -X POST https://api.credda.io/api/v1/confirmations/batch \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/confirmations/{id} public

Fetch one of your confirmation requests

Parameters

id (path) required string

Responses

200Confirmation request · object
404Not found · Error

Example

curl https://api.credda.io/api/v1/confirmations/{id}
post /api/v1/confirmations/{id}/cancel public

Cancel a still-pending request

Parameters

id (path) required string

Responses

200Cancelled · object
404Not found · Error
409Already decided (CONFIRMATION_NOT_PENDING) · Error

Example

curl -X POST https://api.credda.io/api/v1/confirmations/{id}/cancel
get /api/v1/confirmations/{id}/preview public

What the counterparty is being asked to confirm (public, token-gated)

Token-gated and unauthenticated: a counterparty holds the one-time token, not an API key. Returns a PII-free subset (the platform name, the human description, the outcome type/stake/value, the deadline), never the raw subject externalId. Read-only.

Parameters

id (path) required string
token (query) required string: The one-time confirmation token.

Responses

200Preview · object
401Invalid/missing token (CONFIRMATION_TOKEN_INVALID) · Error
404Not found · Error

Example

curl https://api.credda.io/api/v1/confirmations/{id}/preview
post /api/v1/confirmations/{id}/respond public

Counterparty confirms or declines (public, token-gated)

The counterparty presents the one-time token and their decision. On `confirm` the proposed event is written to the ledger with isVerified: true (the confirmation is the third-party witness) and the request becomes CONFIRMED. On `decline` nothing is written and it becomes DECLINED. Single-use: a request already decided or expired is 409 CONFIRMATION_NOT_PENDING. Unauthenticated on purpose: the token is the capability.

Parameters

id (path) required string

Request body

application/jsonobject

Responses

200Decision applied · object
401Invalid token (CONFIRMATION_TOKEN_INVALID) · Error
409Already decided or expired (CONFIRMATION_NOT_PENDING) · Error

Example

curl -X POST https://api.credda.io/api/v1/confirmations/{id}/respond \
  -H "Content-Type: application/json" \
  -d '{ ... }'

Activation

Activation campaigns: the confirmation primitive AT SCALE. Submit your whole historical book (roster / timesheets) in one call; each row becomes an UNCONFIRMED confirmation request fanned out to its named counterparty, and the campaign reports the funnel (submitted → confirmed / declined / pending) as those tokens are acted on. A campaign writes NO events: every row flows through the same create → confirm path a single confirmation uses, so isVerified is earned only on a genuine counterparty confirm, never here. NO VERDICT: a campaign counts asks and confirmations, it never rates the subject.

post /api/v1/activation/campaigns public

Onboard your whole book in one call (the activation engine)

The biggest density unlock: submit your historical roster/timesheets (up to 500 rows) in ONE call. Each row becomes an UNCONFIRMED ConfirmationRequest: a proposed outcome plus a one-time token, fanned out to its named counterparty, turning "manually create 200 confirmations" into a single request. Then GET /activation/campaigns/{id} reports the funnel as those tokens are acted on. INVARIANT: a campaign writes NOTHING to the ledger. Every row goes through the SAME create service a single POST /confirmations uses, so isVerified is still EARNED in exactly one place (a genuine counterparty confirm), never hardcoded here. Importing a book creates pending ASKS, it does not manufacture verified events; each event lands only when ITS counterparty confirms. Partial success (like POST /events/batch): a bad row (e.g. counterpartyRef equals the subject → CONFIRMATION_SELF, or an invalid returnUrl) fails INDIVIDUALLY with its index + code; the rest still land. If NOT ONE row could be created, the campaign is not persisted and the call is 400 ACTIVATION_NO_VALID_ROWS. Give each row a `rowKey` (your own stable id for the roster line) to make the campaign idempotent per row; a duplicate rowKey within one submission is dropped before any token is minted (reported in `duplicates`). Send an `Idempotency-Key` header to make a retried whole submission exactly-once. An over-cap submission (>500 rows) is a 400, rejected whole. Lives on the `events` scope.

Parameters

Idempotency-Key (header) string: Optional; makes a retried submission replay the original response instead of creating a second campaign.

Request body

application/jsonobject

Responses

201Campaign created (per-row tokens shown once) · object
400Validation error, over the 500-row cap, or no row could be created (ACTIVATION_NO_VALID_ROWS) · Error

Example

curl -X POST https://api.credda.io/api/v1/activation/campaigns \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/activation/campaigns/{id} public

Campaign funnel (submitted → confirmed / declined / pending)

Derived LIVE from the campaign's linked confirmation requests, so it always reflects the current state as counterparties act on their tokens. Read-only. Scoped to your platform + key mode; a campaign from another platform (or the other mode) is 404.

Parameters

id (path) required string

Responses

200Campaign + funnel · object
404Not found · Error

Example

curl https://api.credda.io/api/v1/activation/campaigns/{id}

References

Reference / employment-verification requests: the qualifications-half sibling of confirmations. Propose a résumé claim (employment/education/certification/skill) and have the named third party who was there (a past employer, manager, client, colleague, or issuing body) confirm it through a one-time token, turning a self-attested qualification VERIFIED. A pending/declined/expired/cancelled request records nothing; only a genuine confirmation records the qualification (verified, decided by the one witness valve, never hardcoded) and a qualification NEVER moves the reliability score. The confirm/preview endpoints are token-gated and unauthenticated. NO VERDICT: a reference attests a fact (this person held this role / earned this credential), never a rating or recommendation.

get /api/v1/references public

List your reference requests

Parameters

cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.
limit (query) integer
status (query) PENDING | CONFIRMED | DECLINED | EXPIRED | CANCELLED: Filter to one lifecycle state.

Responses

200Reference requests · object

Example

curl https://api.credda.io/api/v1/references
post /api/v1/references public

Propose a résumé claim for a reference to confirm

Creates a PENDING reference request and returns a ONE-TIME token; it records NO qualification and moves NO score. Deliver the link to the named counterparty over your OWN channel (Credda sends nothing to anyone): the hosted page `referenceUrl` (zero frontend to build) or your own UI over `previewUrl`/`respondUrl`. Only when they confirm is the qualification written, with isVerified: true, EARNED because a distinct party who was there confirmed the claim, never asserted (the difference from a self-attested POST /users/{id}/qualifications). `counterpartyRef` must differ from `userId` (a person cannot be their own reference → 400 REFERENCE_SELF). `category` is one of the qualification categories; label/issuer/jurisdiction/reference are display-only and are never scored or ranked (no prestige weighting). Send an `Idempotency-Key` header to make a retried create exactly-once. FREE AT EVERY TIER: any valid platform key may create requests, no scope required (the ask is never paywalled; only a genuine counterparty confirm records an event). INVARIANT: pending/declined/expired requests record nothing; only CONFIRMED records the qualification, and a qualification never moves the reliability score.

Parameters

Idempotency-Key (header) string: Optional; makes a retried create return the original request + token instead of a duplicate.

Request body

application/jsonobject

Responses

201Created (token shown once) · object
400Validation error, or counterpartyRef equals the subject (REFERENCE_SELF) · Error

Example

curl -X POST https://api.credda.io/api/v1/references \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/references/{id} public

Fetch one of your reference requests

Parameters

id (path) required string

Responses

200Reference request · object
404Not found · Error

Example

curl https://api.credda.io/api/v1/references/{id}
post /api/v1/references/{id}/cancel public

Cancel a still-pending request

Parameters

id (path) required string

Responses

200Cancelled · object
404Not found · Error
409Already decided (REFERENCE_NOT_PENDING) · Error

Example

curl -X POST https://api.credda.io/api/v1/references/{id}/cancel
get /api/v1/references/{id}/preview public

What the counterparty is being asked to confirm (public, token-gated)

Token-gated and unauthenticated: a counterparty holds the one-time token, not an API key. Returns a PII-free subset (the platform name, the human description, the claim category/label/issuer/jurisdiction/reference, the deadline), never the raw subject externalId. Read-only.

Parameters

id (path) required string
token (query) required string: The one-time reference token.

Responses

200Preview · object
401Invalid/missing token (REFERENCE_TOKEN_INVALID) · Error
404Not found · Error

Example

curl https://api.credda.io/api/v1/references/{id}/preview
post /api/v1/references/{id}/respond public

Counterparty confirms or declines (public, token-gated)

The counterparty presents the one-time token and their decision. On `confirm` the qualification is recorded with isVerified: true (the reference is the third-party witness, decided by the one witness valve, never hardcoded) and the request becomes CONFIRMED. A qualification never moves the reliability score. On `decline` nothing is recorded and it becomes DECLINED (absence of a reference is not evidence against the claim). Single-use: a request already decided or expired is 409 REFERENCE_NOT_PENDING. Unauthenticated on purpose: the token is the capability.

Parameters

id (path) required string

Request body

application/jsonobject

Responses

200Decision applied · object
401Invalid token (REFERENCE_TOKEN_INVALID) · Error
409Already decided or expired (REFERENCE_NOT_PENDING) · Error

Example

curl -X POST https://api.credda.io/api/v1/references/{id}/respond \
  -H "Content-Type: application/json" \
  -d '{ ... }'

Screenings

Async bulk screening jobs: batch score reads at roster scale (thousands of ids). Strictly read-only: a screening never writes events, snapshots, or anything score-side.

get /api/v1/screenings public

List your screening jobs (status + summary only)

Parameters

cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.
limit (query) integer

Responses

200Screenings · object

Example

curl https://api.credda.io/api/v1/screenings
post /api/v1/screenings public

Submit an async bulk screening (up to 10,000 ids)

Submit a roster of external user ids for bulk score screening. Ids are deduped; each resolves through the SAME read-only lookup as `POST /users/scores`, so the two can never disagree. STRICTLY READ-ONLY: a screening never writes events, snapshots, or anything score-side. Jobs of at most 100 deduped ids are processed inline; the returned job is usually already COMPLETED; larger jobs are queued (poll `GET /screenings/{id}`). The id cap defaults to 10,000 (`SCREENING_MAX_IDS`). If the queue is unavailable, large jobs are refused with 503 SCREENING_QUEUE_UNAVAILABLE rather than accepted and silently never run. Uses the `scores` scope.

Request body

application/jsonobject

Responses

202Accepted (inline jobs return already COMPLETED) · object
400No ids / too many ids · Error
503Queue unavailable for a large job (SCREENING_QUEUE_UNAVAILABLE) · Error

Example

curl -X POST https://api.credda.io/api/v1/screenings \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/screenings/{id} public

Screening status + summary (no results payload)

A job stuck QUEUED/RUNNING past the staleness window (default 15 min, `SCREENING_STALE_MINUTES`), e.g. a service restart lost the queued work, is lazily marked FAILED when read, so it is never silently pending forever.

Parameters

id (path) required string

Responses

200Screening · object
404Not found · Error

Example

curl https://api.credda.io/api/v1/screenings/{id}
get /api/v1/screenings/{id}/results public

Full per-user results (JSON, or ?format=csv attachment)

Parameters

id (path) required string
format (query) json | csv: 'csv' returns a text/csv attachment with a header row and one row per screened user.

Responses

200Results · object
404Not found · Error
409Job not COMPLETED yet, or FAILED (SCREENING_NOT_COMPLETED) · Error

Example

curl https://api.credda.io/api/v1/screenings/{id}/results

Ingest

Data ingress without client-side transformation: POST your OWN payload shape alongside a declarative field mapping, or backfill history from a CSV. Both write through the SAME append-only path as POST /events (idempotency, velocity guard, audit trail, score recomputation); neither contains any scoring logic.

post /api/v1/ingest public

Ingest YOUR payload shape via a declarative field mapping

Send records exactly as your system already shapes them, plus a mapping that says how to reach Credda's fields, no client-side transformation code. Up to 100 records per call (page beyond that, or use `POST /imports` for a historical backfill). Partial success, like `POST /events/batch`: an unmappable or rejected record fails INDIVIDUALLY with its index and reason; the rest still land. Give the mapping an `idempotencyKey` field so re-sending a page is a no-op. The mapping is **declarative data, never code**: a rule may read a dot-path, supply a constant, look a value up in your own table, or apply one of a fixed transform whitelist (`cents_to_units`, `iso_date`, `lowercase`, `trim`, `boolean`). There is no expression language. Mapped records are validated by the SAME schema `POST /events` uses and written through the SAME append-only path (idempotency, velocity guard, audit trail, asynchronous score recomputation). **Verification discipline:** `isVerified` defaults to `false`. A mapping cannot simply assert trust; a record is only ingested verified when the mapping also resolves `verifiedBy` (the third party who witnessed the outcome) to a non-empty value for that record. Otherwise the record is still ingested, downgraded, with an explicit warning.

Request body

application/jsonobject

Responses

200Processed (partial success) · object
400Validation error, or an invalid mapping (INGEST_MAPPING_INVALID) · Error
401Missing/invalid API key · Error
403API key lacks events:write · Error
404Unknown mappingId · Error

Example

curl -X POST https://api.credda.io/api/v1/ingest \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/ingest/mappings public

List your stored mappings

Parameters

cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.
limit (query) integer

Responses

200Mappings · object

Example

curl https://api.credda.io/api/v1/ingest/mappings
post /api/v1/ingest/mappings public

Save a reusable named mapping

Declare your translation once, then post `{ mappingId, records }` forever. A mapping is COPIED onto an import job at submit time, so editing one never rewrites what a past import did. Validated on save.

Request body

application/jsonobject

Responses

201Created · object
400Invalid mapping (INGEST_MAPPING_INVALID) · Error
409A mapping with that name already exists · Error

Example

curl -X POST https://api.credda.io/api/v1/ingest/mappings \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/ingest/mappings/{id} public

Fetch one stored mapping

Parameters

id (path) required string

Responses

200Mapping · object
404Not found · Error

Example

curl https://api.credda.io/api/v1/ingest/mappings/{id}
delete /api/v1/ingest/mappings/{id} public

Delete a stored mapping (config, not ledger data; events already ingested are untouched)

Parameters

id (path) required string

Responses

204Deleted
404Not found · Error

Example

curl -X DELETE https://api.credda.io/api/v1/ingest/mappings/{id}
get /api/v1/imports public

List your CSV imports (status + counts)

Parameters

cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.
limit (query) integer

Responses

200Imports · object

Example

curl https://api.credda.io/api/v1/imports
post /api/v1/imports public

Backfill historical outcomes from a CSV

Upload a CSV of past outcomes with a mapping whose paths are COLUMN NAMES. Rows run through the identical mapping engine as `POST /ingest` and are written through the same append-only path. Two body forms: `text/csv` with `?mappingId=` naming a stored mapping, or `application/json` with `{ csv, mapping | mappingId }`. Async: files of at most 100 rows are processed inline (the 202 usually already reads COMPLETED); larger files are queued; poll `GET /imports/{id}`. If the queue is unavailable a large file is REFUSED with 503 rather than accepted and never run. Row cap 50000 (`IMPORT_MAX_ROWS`). An over-cap file is refused with both numbers; an import is never silently truncated. **Backfill honesty:** imported events keep their REAL `completedAt`/`dueDate`, so the deterministic engine recomputes over true history (recency decay included). An import is not a shortcut to a fresher-looking record. **Verification discipline:** `isVerified` defaults to `false` and is only honoured for a row whose mapping resolves `verifiedBy` (counterparty evidence) on that row. Rows without it still import, downgraded, with a warning on `GET /imports/{id}/errors`.

Parameters

mappingId (query) string: Required for a `text/csv` body; ignored when the JSON body carries `mapping`/`mappingId`.

Request body

application/jsonobject

Responses

202Accepted (inline imports return already COMPLETED) · object
400IMPORT_CSV_INVALID / IMPORT_TOO_MANY_ROWS / INGEST_MAPPING_INVALID · Error
401Missing/invalid API key · Error
403API key lacks events:write · Error
404Unknown mappingId · Error
503IMPORT_QUEUE_UNAVAILABLE: large imports cannot be accepted right now · Error

Example

curl -X POST https://api.credda.io/api/v1/imports \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/imports/{id} public

Import status + counts

Parameters

id (path) required string

Responses

200Import · object
404Not found · Error

Example

curl https://api.credda.io/api/v1/imports/{id}
get /api/v1/imports/{id}/errors public

Per-row failures (and warnings) so you can fix and re-upload

Every rejected row with its 1-based data-row number and the exact reason, plus non-fatal warnings such as an `isVerified` downgrade. Re-uploading a corrected file is safe when the mapping supplies an `idempotencyKey`; rows already written come back as `skipped`. The stored list is capped (`IMPORT_MAX_STORED_ERRORS`); `failedCount` stays authoritative and `truncated` says when the list is short.

Parameters

id (path) required string
limit (query) integer
offset (query) integer

Responses

200Row errors · object
404Not found · Error

Example

curl https://api.credda.io/api/v1/imports/{id}/errors

Agents

Non-human subjects and their counterparty-confirmed delivery receipts. Same ledger, same deterministic formula, one extra integrity rule: an agent's own operator can never confirm its work. A delivery record, not a safety, alignment or capability rating, and never a recommendation.

get /api/v1/verify/{token}/delivery-receipts public

Delivery receipts + a signed delivery credential

The counterparty-confirmed delivery record behind a share token: deliveries recorded, how many a DISTINCT counterparty confirmed, how many were the subject's own operator vouching for it (never confirmed), failures, disputes and the on-time rate over confirmed deliveries, plus a signed W3C credential of that record (`CreddaDeliveryReceiptCredential`, and `CreddaAgentDeliveryCredential` when the subject is an agent), on the same EdDSA key, did:web issuer and StatusList2021 revocation entry as every other Credda credential. Public: the token is the capability, so an agent can present one string mid-negotiation and the counterparty verifies it offline. This is a DELIVERY RECORD, not a safety, alignment or capability rating, and never a recommendation; the payload says so itself.

Parameters

token (path) required string
ttl (query) integer

Responses

200Delivery receipts + credential · object
403Test-mode subject · Error
404Unknown/rotated token · Error

Example

curl https://api.credda.io/api/v1/verify/{token}/delivery-receipts
post /api/v1/agents public

Register (or update) an agent subject

Label a subject as a NON-HUMAN agent and declare who operates it. Writes no events and touches no score; an agent's record runs the IDENTICAL deterministic formula as a person's. Declaring yourself the operator (the default) means every event YOU report for this agent is recorded but never counted as verified evidence: only a distinct counterparty can confirm a delivery. Declaring a third-party operator requires naming it (`operator.name` / `.homepage` / `.did`), and a declared identity that resolves to your own platform is treated as self-dealing too. Refuses to relabel a person subject that already has a reported record (409).

Request body

application/jsonobject

Responses

201Agent subject · object
400Third-party operator declared but not named (OPERATOR_NOT_DECLARED) · Error
409Subject already has a person record · Error

Example

curl -X POST https://api.credda.io/api/v1/agents \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/agents/{id} public

Inspect an agent subject

The agent's declared facts (claims, never evidence), its current deterministic score, and its delivery record split by whether a distinct counterparty confirmed each delivery. Read-only.

Parameters

id (path) required string

Responses

200Agent subject · object
404Not an agent / not found · Error

Example

curl https://api.credda.io/api/v1/agents/{id}

Businesses

Businesses as first-class scored subjects. Same ledger, same deterministic formula, one extra integrity rule: a business can never confirm its own outcomes, not by reporting them itself, and not by naming itself as its own counterparty. A record of outcomes OTHER businesses confirmed. Business-to-business only: Credda holds no customer/patient/consumer register, and a business's record is never derived from the people who work there.

post /api/v1/businesses public

Register (or update) a business subject

Label a subject as a BUSINESS (an ORGANIZATION subject) and declare who controls the record. Writes no events and touches no score; a business's record runs the IDENTICAL deterministic formula as a person's. There is no business score, no business weighting and no business-specific event type. Declaring yourself the controller (the default) means every outcome YOU report for this business is recorded but never counted as confirmed evidence: only a DISTINCT counterparty business can confirm an outcome. An outcome whose recorded counterparty resolves to the business itself (any alias: trading name, domain, did:web) is likewise recorded but never confirmed. At least one identity (`business.legalName` / `.tradingName` / `.homepage` / `.did`) is required, because a business with no identifiable name cannot be checked for self-dealing against its own counterparties. Refuses to relabel a subject that already has a reported record as a person or agent (409). SCOPE: business-to-business only. Credda records no consumer/customer/patient register of any kind, and derives nothing about a business from the people who work there. See docs/BUSINESSES.md.

Request body

application/jsonobject

Responses

201Business subject · object
400No business identity declared (BUSINESS_IDENTITY_NOT_DECLARED) · Error
409Subject already has a person/agent record · Error

Example

curl -X POST https://api.credda.io/api/v1/businesses \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/businesses/{id} public

Inspect a business subject

The business's declared facts (claims, never evidence), its current deterministic score, and its business-to-business record split by whether a distinct counterparty business confirmed each outcome. Read-only. Nothing in the response is derived from the business's staff or its customers.

Parameters

id (path) required string

Responses

200Business subject · object
404Not a business / not found · Error

Example

curl https://api.credda.io/api/v1/businesses/{id}
get /api/v1/businesses/{id}/record public

The counterparty-confirmed B2B record on its own

Outcomes recorded, how many a DISTINCT counterparty business confirmed (`confirmedOutcomes`: the only number that is evidence), how many the business attested about itself (never evidence), failures, disputes, the on-time rate over confirmed fulfilments, the record's own `verificationDepth` (confirmed ÷ total: depth, not platform breadth, matching scoring v5.3), and `distinctConfirmingCounterparties` (aliases collapsed, so one counterparty confirming forty jobs never reads as forty).

Parameters

id (path) required string

Responses

200Business record · object
404Not a business / not found · Error

Example

curl https://api.credda.io/api/v1/businesses/{id}/record
get /api/v1/verify/{token}/business-record public

Business record + a signed business credential

The counterparty-confirmed business-to-business record behind a share token, plus a signed W3C credential of that record (`CreddaBusinessRecordCredential`) on the same EdDSA key, did:web issuer and StatusList2021 revocation entry as every other Credda credential. Public: the token is the capability, so a business can hand a prospective counterparty one string and have the record verified offline. This is a RECORD OF CONFIRMED OUTCOMES, never a rating of the business, never a credit or hiring decision, never anything about the business's customers, and never derived from its staff; the payload says so itself. 404s when the subject behind the token is not a business.

Parameters

token (path) required string
ttl (query) integer

Responses

200Business record + credential · object
403Test-mode subject · Error
404Unknown/rotated token, or not a business · Error

Example

curl https://api.credda.io/api/v1/verify/{token}/business-record

Disputes

Resolve a dispute on one of your events.

patch /api/v1/disputes/{id}/resolve public

Resolve a dispute

Parameters

id (path) required string

Request body

application/jsonobject

Responses

200Resolved
403Not your event · Error
409Already resolved · Error

Example

curl -X PATCH https://api.credda.io/api/v1/disputes/{id}/resolve \
  -H "Content-Type: application/json" \
  -d '{ ... }'

Earnings

Verified Earnings: a portable attestation of income already recorded on the ledger, plus a signed, offline-verifiable earnings credential. Pure derivation: no scoring logic, and nothing here can move a score. Not an income verification for a credit decision and not a consumer report.

get /api/v1/users/{id}/earnings public

Verified earnings attestation (monthly breakdown + stability metrics)

Attests income ALREADY RECORDED on the ledger. Monthly buckets over the requested window, each with attested gross, event count and per-platform breakdown, plus stability metrics lenders care about (months with earnings, median/mean monthly, coefficient of variation, longest consecutive run, trailing-12m total). **Only counterparty/platform-VERIFIED outcomes are attested.** Unverified activity is reported separately as `unverifiedReported` and is never blended into any attested figure. Disputed outcomes are excluded (and counted in `excluded`). Auto-imported activity still counts but is surfaced via `coverage.selfReportedShare`. `currency` is always **null**: the ledger records no currency, so amounts are platform-reported units. **This is an attestation of recorded outcomes. It is NOT an income verification for a credit decision, NOT a consumer report, and Credda makes no representation of completeness; the subject may earn income that was never reported here. Nothing is extrapolated or projected.** No verdict language appears anywhere in the response. Pure derivation over the same ledger the score reads: this surface reads no score, writes nothing, and cannot influence a score.

Parameters

id (path) required string
months (query) integer: Trailing window length in months (1–120). Ignored when `from` is supplied. Default 12.
from (query) string: Window start (ISO-8601). Overrides `months`.
to (query) string: Window end (ISO-8601). Defaults to now.

Responses

200Verified earnings · VerifiedEarnings
400Invalid window · Error
404User not found · Error

Example

curl https://api.credda.io/api/v1/users/{id}/earnings
get /api/v1/users/{id}/earnings/summary public

Compact earnings summary (trailing 12m total, median monthly, volatility, coverage)

The same attestation reduced to the handful of figures a lender or landlord actually reads. Adds no fact the full breakdown does not already hold. Carries the same disclosures and the same `currency: null`.

Parameters

id (path) required string
months (query) integer: Trailing window length in months (1–120). Ignored when `from` is supplied. Default 12.
from (query) string: Window start (ISO-8601). Overrides `months`.
to (query) string: Window end (ISO-8601). Defaults to now.

Responses

200Earnings summary · EarningsSummary
400Invalid window · Error
404User not found · Error

Example

curl https://api.credda.io/api/v1/users/{id}/earnings/summary
post /api/v1/users/{id}/earnings/credential public

Mint a signed Verified Earnings Credential (W3C VC-JWT)

Issues an EdDSA-signed W3C Verifiable Credential of type `CreddaEarningsCredential` carrying the summary claims, the window, and the `earningsVersion` provenance, so the subject can PROVE the recorded income offline, without the verifier calling Credda. Same issuer key and `did:web` identity as the trust credential, and it carries a StatusList2021 `credentialStatus` so it can be revoked. Uses the subject's existing share token when there is one (issuing a credential never rotates a token and never invalidates a published badge); mints one if they have none. **Refuses test-mode users** (`TEST_MODE_NOT_ALLOWED`): sandbox data never becomes portable trust. Writes nothing score-side. The credential attests recorded outcomes; it is not an income verification for a credit decision and not a consumer report.

Parameters

id (path) required string

Request body

application/jsonobject

Responses

201Signed earnings credential · object
400Invalid window or ttl · Error
403Test-mode keys cannot issue credentials · Error
404User not found · Error

Example

curl -X POST https://api.credda.io/api/v1/users/{id}/earnings/credential \
  -H "Content-Type: application/json" \
  -d '{ ... }'

Professional Record

A worker-OWNED, résumé-shaped summary of a VERIFIED work record: reliability band, verified-outcome counts, verification depth and tenure, plus a signed, offline-verifiable credential and a LinkedIn "Add to profile" deep link. Pure derivation: no scoring logic, nothing here can move a score. It describes the record the subject chose to present; it is NOT a hiring recommendation, a background check, or a consumer report.

get /api/v1/users/{id}/professional-record public

Worker-owned summary of a verified work record (résumé-shaped)

A portable summary of the subject's VERIFIED work record: reliability score/band + verified-evidence confidence, verified-outcome counts, `verificationDepth` (share of the record independently verified), verified-platform breadth, and `tenure` (the observed span of the record). For everyone with a work history, not only contractors. **Only THIRD-PARTY-VERIFIED outcomes count as verified experience.** Self-attested activity is recorded in `totalOutcomes` but is never counted as verified. Nothing is invented, extrapolated or projected; a missing figure is `null`, never a default. **It describes the record the subject chose to present. It is NOT a hiring, promotion or employment recommendation, NOT a background check, and NOT a consumer report under the FCRA. No verdict language appears anywhere in the response.** Pure derivation over the same ledger the score reads: this surface reads no score, writes nothing, and cannot influence a score.

Parameters

id (path) required string

Responses

200Professional record · ProfessionalRecord
404User not found · Error

Example

curl https://api.credda.io/api/v1/users/{id}/professional-record
post /api/v1/users/{id}/professional-record/credential public

Mint a signed Professional Record Credential + an "Add to LinkedIn" link

Issues an EdDSA-signed W3C Verifiable Credential of type `CreddaProfessionalRecordCredential` carrying the summary claims and the `professionalRecordVersion` provenance, so the subject can PROVE their verified record offline, on a résumé, a profile, or in a wallet. Same issuer key and `did:web` identity as every other Credda credential, with a StatusList2021 `credentialStatus` so it can be revoked. Also returns a `linkedin` block with an **"Add to profile" certification deep link**. LinkedIn does not ingest verifiable credentials; the link opens its "Add licenses & certifications" form pre-filled with a certification whose "Show credential" URL (`certUrl`) resolves to a public, independently verifiable Credda proof (`GET /api/v1/verify/{token}`). The signed credential carries the claims; LinkedIn stores only the name, organization, dates, credential id and the verification URL. Uses the subject's existing share token when there is one (never rotates it, so a published badge keeps working); mints one if they have none. **Refuses test-mode users** (`TEST_MODE_NOT_ALLOWED`): sandbox data never becomes portable trust. Writes nothing score-side. It attests a record; it is not a hiring recommendation and not a consumer report.

Parameters

id (path) required string

Request body

application/jsonobject

Responses

201Signed professional record credential · object
400Invalid ttl · Error
403Test-mode keys cannot issue credentials · Error
404User not found · Error

Example

curl -X POST https://api.credda.io/api/v1/users/{id}/professional-record/credential \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/users/{id}/career-export public

Portable professional record in the open JSON Resume schema

The subject's whole verified professional record: reliability + verified experience + tenure + itemised qualifications (work history, education, certifications, skills), serialised in an OPEN schema so it drops into an ATS/HRIS, a résumé builder, or an LER-style reader **without a bespoke Credda integration**. Two open serialisations are available: **JSON Resume** (`format=jsonresume`, jsonresume.org; the default) and a **schema.org `Person` JSON-LD graph** (`format=jsonld`, schema.org: `EducationalOccupationalCredential`, `alumniOf`, `hasOccupation`, `knowsAbout`). Both carry the same facts; pick whichever your reader already parses. **What makes it Credda's and not just a résumé:** every item is flagged `verified` vs `self-reported` (mirroring the ledger's `isVerified` exactly: a per-item `credda` extension in JSON Resume, `credda:verified` on each node in JSON-LD), and each verified item anchors to the subject's public **proof URL** (`GET /api/v1/verify/{token}`) so a reader can confirm it independently. The document therefore transports third-party verification in a shape existing tools already accept. A `meta.credda` / `credda:meta` block carries the reliability summary, verification depth, tenure, provenance and the always-present disclosures. The subject's EXISTING share token is used to build proof URLs when they have one (issuing an export never mints or rotates a token); with no token (including every sandbox user), verified items carry no public anchor but the document is still valid. **It describes a record. It is NOT a hiring, lending, or employment decision or an input to one, NOT a background check, and NOT a consumer report under the FCRA. No verdict language appears anywhere.** Read-only under `scores:read`; reads no score and writes nothing.

Parameters

id (path) required string
format (query) jsonresume | jsonld: Open serialisation: 'jsonresume' (default) or 'jsonld' (schema.org Person).

Responses

200JSON Resume document (or schema.org JSON-LD when format=jsonld), each item flagged verified vs self-reported · object
400Unsupported format (CAREER_FORMAT_INVALID) · Error
404User not found · Error

Example

curl https://api.credda.io/api/v1/users/{id}/career-export
get /api/v1/users/{id}/reliability-report public

One-call worker reliability report (decision-support dossier)

A single consolidated read a staffing agency or employer weighs before placing or hiring a worker. It **aggregates** what the engine already computed; it computes no new score: - `reliability`: score, band, verified-evidence confidence, and the `formulaVersion` / `reasonCodesVersion` that produced them. - `metrics`: the reliability rates a buyer reads first: `completionRate`, `onTimeRate`, `consistency`, `recency` (null when there is no dated activity) and `disputeRate`, each ∈ [0,1] and a pure relabel of the snapshot's own components. - `verifiedExperience`: verified vs total outcomes, `verificationDepth`, verified-platform breadth, and `tenure` (the observed span of the record). - `topFactors`: the ranked reason-code drivers (WHY the score is what it is), merged across adverse/supporting and ordered by contribution. - `recentOutcomes`: the last N confirmed outcomes, **each flagged `verified` vs `self_reported`** so self-reported activity is never presented as verified. - `benchmark` (with `?benchmark=1`): a COARSE quartile-grain comparison only (top_decile / top_quartile / above_median / below_median), k-anonymity respected; null below the floor or with no score. **FCRA bright line:** it is EVIDENCE for a reader who applies their own criteria, NOT a hire / place / rank / approve / deny verdict or recommendation. The always-present `disclosures` state this is not a consumer report, that Credda decides nothing, and that the reader owns the decision. No verdict language appears anywhere in the payload. Read-only under `scores:read`; reuses already-computed values and cannot influence a score (`calculateCreddaScore` stays the sole snapshot writer). Test/live isolated and book-scoped like every other `/users/{id}` read.

Parameters

id (path) required string
recent (query) integer: Recent outcomes to include (1–50, default 10).
benchmark (query) 1: Set to 1 to attach the coarse quartile-grain benchmark.

Responses

200Consolidated reliability report · object
404User not found · Error

Example

curl https://api.credda.io/api/v1/users/{id}/reliability-report
get /api/v1/users/{id}/projects public

List the subject's personal projects (verified vs self-reported)

The subject's self-presented personal projects: a photo record of finished work, a live URL, a case study, a code repository, each flagged verified vs self-reported. **A project is never a scoring input** (a `PROJECT_LOGGED` event is not a reliability-outcome type), and Credda stores the link as text and never fetches it. Read-only under `scores:read`.

Parameters

id (path) required string

Responses

200Personal projects · PersonalProjects
404User not found · Error

Example

curl https://api.credda.io/api/v1/users/{id}/projects
post /api/v1/users/{id}/projects public

Record a personal project (self-reported by default)

Records a personal project on the append-only ledger. The project is ALWAYS recorded; whether it counts as VERIFIED is decided by the witness rule: `isVerified` is true ONLY when a genuine third-party `verifiedBy` witness (distinct from the subject) is supplied, NEVER hardcoded true. The `url` must be an absolute **http(s)** link to a **public** host: `javascript:`/`data:`/`file:` schemes, embedded credentials, and private/loopback/link-local hosts are rejected. **Credda never fetches or scrapes the link**; the scheme check only governs what may be stored. **Writes nothing score-side**; a personal project cannot move the Reliability Score.

Parameters

id (path) required string

Request body

application/jsonobject

Responses

201Project recorded · object
400Invalid project (missing title, or url is not a public http(s) link) · Error

Example

curl -X POST https://api.credda.io/api/v1/users/{id}/projects \
  -H "Content-Type: application/json" \
  -d '{ ... }'

Verified Profile

The QUALIFICATIONS half of the professional record: education, skills, certifications, employment. A deterministic, BIAS-FREE measure of how much of the claimed record is independently third-party verified. It counts WHETHER a claim is verified, never how prestigious it is: no school, employer, degree or credential is ranked or weighted. Complements the Professional Record tag (which summarises the RELIABILITY record). A SEPARATE measure from the Reliability Score that can never move it, and verification of claims, not an assessment of the person.

get /api/v1/users/{id}/verified-profile public

Verified-profile measure (verification depth of the qualifications record)

How much of the subject's claimed QUALIFICATIONS record (education, skills, certifications, employment) is independently third-party verified. Returns per-category claimed vs verified counts, each category's verification depth, and the overall depth (share of the whole claimed record that is verified). **Deterministic and BIAS-FREE.** It counts WHETHER each claim is verified, never how prestigious it is: a verified credential from a community college and a verified degree from any other institution count identically. No school, employer, degree or credential is ranked or weighted; that would reintroduce exactly the bias the score exists to remove. Self-attested claims are recorded but do not raise verification depth. Complements `GET /api/v1/users/{id}/professional-record`, which summarises the RELIABILITY record (outcome events and tenure). Different populations, different endpoints. **This is verification of claims, not an assessment of the person, and it is a SEPARATE measure from the Reliability Score; it can never move a score.** Read-only under `scores:read`; nothing here computes or writes a score.

Parameters

id (path) required string

Responses

200Verified profile · VerifiedProfile
404User not found · Error

Example

curl https://api.credda.io/api/v1/users/{id}/verified-profile
post /api/v1/users/{id}/qualifications public

Record a qualification claim (education / skill / certification / employment)

Records a qualifications claim for the subject on the append-only ledger. The claim is ALWAYS recorded; whether it counts as VERIFIED is decided by the witness rule: `isVerified` is true ONLY when a genuine third-party `verifiedBy` witness (distinct from the subject) is supplied; it is NEVER hardcoded true, and a claim without a witness (or witnessed by the subject) is recorded as self-attested. There is NO prestige/rank field: a claim carries free-text `label`, `issuer`, `jurisdiction` and `reference` for display only, and none of them is ever a scoring input or ranked. **Writes nothing score-side and never enqueues a score recompute**; a qualification claim cannot move the Reliability Score. `jurisdiction` and `reference` make a LICENCE expressible: a trade or professional licence is meaningless without the place that granted it and the number printed on it. Both are properties of the CREDENTIAL; they are stored in that claim's ledger metadata, are never written to the subject, are never indexed and are never queryable, so a jurisdiction can describe a credential without ever becoming a way to filter people by where they are from. No jurisdiction ranks above another, and Credda does not resolve a `reference` against any register or assert that it is current.

Parameters

id (path) required string

Request body

application/jsonobject

Responses

201Qualification recorded · object
400Invalid category · Error

Example

curl -X POST https://api.credda.io/api/v1/users/{id}/qualifications \
  -H "Content-Type: application/json" \
  -d '{ ... }'
post /api/v1/users/{id}/qualifications/import public

Bulk-import a claimed professional record (education / employment / skills / certifications)

The onboarding accelerator: seed a whole claimed professional record in ONE call instead of one POST per line. Takes a STRUCTURED array of claims; there is deliberately NO AI and NO free-text résumé parsing here (this is the deterministic scoring API; that advisory step belongs elsewhere). Up to 100 claims per call (more is a 400). Each item is the same shape as `POST /users/{id}/qualifications`, and every item flows through the SAME single-claim writer, so `isVerified` is decided by the witness rule in exactly one place and is NEVER hardcoded true. A self-import with no witnesses records every claim self-attested (`isVerified:false`); because verification depth is verified/claimed, a big self-attested import LOWERS depth until each claim is independently confirmed; it seeds claims, it does not manufacture trust. Partial-success: one bad row does not roll back the rest; each item carries its own `ok`/`error`. **Writes nothing score-side and never enqueues a score recompute**; a qualification claim cannot move the Reliability Score. Prototype-pollution keys are refused.

Parameters

id (path) required string

Request body

application/jsonobject

Responses

200Import summary (partial success) · QualificationImportResult
400Malformed body, over the per-import cap, or an invalid claim · Error

Example

curl -X POST https://api.credda.io/api/v1/users/{id}/qualifications/import \
  -H "Content-Type: application/json" \
  -d '{ ... }'

Verify

Public, PII-free verification + Verifiable Credentials.

post /api/v1/users/{id}/share-token public

Mint/rotate a public trust-badge token

Parameters

id (path) required string

Responses

200Token + embed snippet · object

Example

curl -X POST https://api.credda.io/api/v1/users/{id}/share-token
delete /api/v1/users/{id}/share-token public

Revoke the share token

Parameters

id (path) required string

Responses

200Revoked

Example

curl -X DELETE https://api.credda.io/api/v1/users/{id}/share-token
get /api/v1/verify/{token} public

Resolve a share token to a PII-free payload + credential

Public trust payload for the share token, reduced to the requested disclosure scope, plus a signed credential over exactly those disclosed facts. `?earnings=1` additionally attaches a compact Verified Earnings summary, **only at `scope=full`** (a band/minimal embed must never leak income) and **never inside the signed credential**; the signed income attestation is the dedicated `POST /users/{id}/earnings/credential`. The block is `null` if earnings are unavailable. `?professional=1` attaches the worker-owned Professional Record summary, **only at `scope=full`**, never inside the signed credential (the signed attestation is `POST /users/{id}/professional-record/credential`). The block is `null` if unavailable. `?profile=1` attaches the CONSOLIDATED professional profile (the "portable resume" / AI-recruiter read): reliability + verified qualifications (work history / education / certifications / skills, itemized) + linked personal projects, each flagged verified vs self-reported, with the FCRA/no-verdict disclosures in-band. The share token IS the consent. **Only at `scope=full`**, never inside the signed credential, `null` if unavailable. It is evidence a reader verifies and applies their own criteria to, never a rating, ranking, or hiring verdict on the person.

Parameters

token (path) required string
scope (query) full | band | minimal
earnings (query) 1: Set to 1 to attach a Verified Earnings summary (scope=full only).
professional (query) 1: Set to 1 to attach the Professional Record summary (scope=full only).
profile (query) 1: Set to 1 to attach the consolidated professional profile: reliability + verified qualifications + linked projects (scope=full only).
benchmark (query) 1: Set to 1 to attach a COARSE population comparison (scope=full only): a quartile-grain label (`top_decile` / `top_quartile` / `above_median` / `below_median`) against the whole scored population, never a precise percentile and never the population size. Absent (`null`) unless the population clears the k-anonymity floor. A distribution fact, not a verdict.

Responses

200Verification payload · object
404Unknown/rotated token · Error

Example

curl https://api.credda.io/api/v1/verify/{token}
get /api/v1/verify/{token}/credential public

Signed credential (?format=w3c → W3C VC-JWT)

Parameters

token (path) required string
format (query) credda | w3c

Responses

200Credential · object

Example

curl https://api.credda.io/api/v1/verify/{token}/credential
get /api/v1/verify/{token}/export public

Portable, self-verifying trust export

A single bundle the subject owns: current public score + a signed W3C credential (offline-verifiable) + a revocation pointer. Public: the token is the capability.

Parameters

token (path) required string

Responses

200Trust export bundle · object
404Unknown/rotated token · Error

Example

curl https://api.credda.io/api/v1/verify/{token}/export
get /api/v1/verify/{token}/career-export public

Portable professional record in an open résumé schema

The subject's whole verified professional record: reliability + verified experience + tenure + itemised qualifications (work history / education / certifications / skills), serialised in an OPEN schema so it drops into an ATS/HRIS or résumé tool without a bespoke Credda integration. Two open serialisations: **JSON Resume** (`format=jsonresume`, jsonresume.org; default) and a **schema.org `Person` JSON-LD graph** (`format=jsonld`, schema.org). Every item is flagged `verified` vs `self-reported`, and verified items anchor to THIS public proof URL so a reader can confirm them independently. A `meta.credda` / `credda:meta` block carries the reliability summary, verification depth, tenure and the always-present disclosures. The share token IS the consent. **It describes a record, never a hiring/lending decision, a background check, or an FCRA consumer report; no verdict language appears.** Public: the token is the capability; read-only.

Parameters

token (path) required string
format (query) jsonresume | jsonld

Responses

200JSON Resume document (or schema.org JSON-LD when format=jsonld) · object
400Unsupported format (CAREER_FORMAT_INVALID) · Error
404Unknown/rotated token or no record · Error

Example

curl https://api.credda.io/api/v1/verify/{token}/career-export
get /api/v1/verify/{token}/reliability-report public

Worker-consent reliability report

The WORKER-CONSENT variant of `GET /api/v1/users/{id}/reliability-report`: a worker hands a prospective employer their share token and the employer reads the same consolidated decision-support dossier: reliability + metrics + verified experience + tenure + ranked drivers + recent outcomes (each flagged verified vs self-reported) + an optional coarse benchmark, with NO API key. The token IS the consent to share it. Fail-safe: an unknown/rotated token 404s; a race that removes the record returns `reliabilityReport: null` rather than a broken document. `?recent=N` (1–50, default 10) bounds the outcomes list; `?benchmark=1` attaches the coarse quartile-grain comparison. **It is evidence a reader weighs against their own criteria, never a hiring verdict, a background check, or an FCRA consumer report; no verdict language appears.** Public: the token is the capability; read-only.

Parameters

token (path) required string
recent (query) integer
benchmark (query) 1

Responses

200Reliability report (or reliabilityReport: null) · object
404Unknown/rotated token · Error

Example

curl https://api.credda.io/api/v1/verify/{token}/reliability-report
get /api/v1/status/revocation public

StatusList2021 revocation list (signed VC)

Published bitstring of revoked credential subjects. A bit is set when the subject revoked their share token or the score is frozen. Verifiers check the `credentialStatus` index against this list offline.

Responses

200StatusList2021 Verifiable Credential · object

Example

curl https://api.credda.io/api/v1/status/revocation

Widget

Dependency-free browser embeds, served from this origin (never a CDN subdomain): the display-only trust badge, and the "Confirm with Credda" launcher that opens the hosted confirmation flow in a sandboxed modal and reports a status back to the host page.

get /widget/credda-widget.js public

Embeddable trust-badge script

Dependency-free browser script (`@credda/widget`): drop `<div data-credda-badge data-token="…">` + this script tag on any page to render a live, shadow-DOM-isolated trust badge from a public share token.

Responses

200JavaScript source

Example

curl https://api.credda.io/widget/credda-widget.js
get /verify-embed.js public

Embeddable "Confirm with Credda" launcher

Dependency-free browser script: the ACTION sibling of the trust badge above. Drop this tag plus `<credda-verify confirmation-id="cnf_…" token="…">` (or call `CreddaVerify.open({ confirmUrl })` with the `confirmUrl` from `POST /api/v1/confirmations`) and the hosted confirmation page opens in a **sandboxed modal iframe** on your own page. Completion is reported to your page by `postMessage` as a **status only**: `confirmed` / `declined` / `dismissed`. No token, subject id, counterparty reference or score ever crosses to the host page (the same discipline as the `credda_confirmation` marker on `returnUrl`). The launcher validates `event.origin` against `https://api.credda.io` **and** that the message came from the exact iframe it opened; anything else is ignored. Served from this origin on purpose, never a `cdn.*` subdomain.

Responses

200JavaScript source

Example

curl https://api.credda.io/verify-embed.js
get /verify-embed-bridge.js public

Completion bridge for the embedded confirmation flow

Internal to the launcher; you never reference this directly. It runs on api.credda.io inside the modal iframe, on the terminal page of an embedded confirmation, and posts the status to the embedding page. Documented because it is a public URL this host serves.

Responses

200JavaScript source

Example

curl https://api.credda.io/verify-embed-bridge.js

Well-Known

Public keys, DID document, trust registry, Web Bot Auth signature directory.

get /.well-known/jwks.json public

Verifying keys (Ed25519/OKP)

Responses

200JWKS · object

Example

curl https://api.credda.io/.well-known/jwks.json
get /.well-known/did.json public

did:web issuer DID document

Responses

200DID document · object

Example

curl https://api.credda.io/.well-known/did.json
get /.well-known/credda-trust-registry.json public

Recognized issuers

Responses

200Trust registry · object

Example

curl https://api.credda.io/.well-known/credda-trust-registry.json
get /.well-known/http-message-signatures-directory public

Web Bot Auth signature directory (Ed25519 JWKS)

The public Ed25519 keys that sign Credda's **outbound webhook deliveries** as identified automated traffic, per draft-meunier-http-message-signatures-directory-03 and draft-meunier-web-bot-auth-architecture-02 over RFC 9421 (2024-02). Bot-mitigation layers (Cloudflare, AWS WAF, Akamai) fetch this to verify the `Signature` / `Signature-Input` / `Signature-Agent` headers on a delivery. **These are NOT the credential-issuing keys.** `/.well-known/jwks.json` publishes the keys that sign Verifiable Credentials; this directory publishes a transport identity with a different rotation cadence and a different compromise profile. Never accept a key from here as a credential issuer key. Served as `application/http-message-signatures-directory+json` with `Cache-Control: max-age=86400`, and (when a key is configured) with its own `Signature`/`Signature-Input` over `("@authority";req)` tagged `http-message-signatures-directory`. Web Bot Auth is **inert by default**: with no signing key configured, `keys` is an empty array and deliveries carry no RFC 9421 headers. The HMAC `X-Credda-Signature` scheme is unaffected either way: it is what proves the payload is authentic, and it is what an integrator verifies. See docs/WEBHOOKS.md.

Responses

200Signature directory (JWKS)

Example

curl https://api.credda.io/.well-known/http-message-signatures-directory

Wallets

Wallet-compatible issuance: OpenID for Verifiable Credential Issuance (openid-4-verifiable-credential-issuance-1_0 (final, 2025-09-16)) with SD-JWT VC (draft-ietf-oauth-sd-jwt-vc-17), so a holder can keep their Credda credential in any OID4VCI wallet instead of coming back to Credda for it. Selective disclosure moves the "how much do I reveal" choice from issuance to presentation. Issuance is implemented; the OID4VP request side is not (see docs/WALLETS.md).

post /api/v1/users/{id}/credential-offer public

Mint an OID4VCI Credential Offer for this subject

Returns a Credential Offer (openid-4-verifiable-credential-issuance-1_0 (final, 2025-09-16), Pre-Authorized Code flow) plus its `openid-credential-offer://` deep link, so the subject can collect their Credda credential into any OID4VCI-capable wallet: show it as a QR code or hand it over as a link. The offer carries a **single-use, short-lived** pre-authorized code; everything after that happens between the wallet and `/oid4vci/*`, so this is the only endpoint a platform integrates with. `scope` (minimal | band | full, default `full`) bounds what the credential CONTAINS. For an SD-JWT VC the holder additionally chooses, at presentation time, which of those claims to reveal: issuance scope bounds what exists, presentation choice bounds what is shown. Uses the subject's existing share token when there is one (never rotates it, so a published badge keeps working). **Refuses test-mode keys** (`TEST_MODE_NOT_ALLOWED`): sandbox data never becomes portable trust. INVARIANT: no score is computed or adjusted here.

Parameters

id (path) required string

Request body

application/jsonobject

Responses

201Credential offer · object
400Unknown credential configuration or scope · Error
403Test-mode keys cannot issue credentials · Error
404User not found · Error

Example

curl -X POST https://api.credda.io/api/v1/users/{id}/credential-offer \
  -H "Content-Type: application/json" \
  -d '{ ... }'
post /api/v1/verify/presentation public

Verify an SD-JWT VC presentation a holder handed you

The relying-party half of a wallet handshake. A holder presents an SD-JWT VC (collected from Credda by any OID4VCI wallet) with only the disclosures they chose to reveal; this checks the issuer signature, enforces expiry, and recomputes every supplied disclosure against `_sd`; a fabricated or tampered disclosure is an error, never a silently-accepted claim. Returns exactly the disclosed claims, plus the `credentialStatus` pointer so you can check the published StatusList2021 for revocation. Key Binding JWTs are not required or checked today. This is deliberately NOT full OID4VP: there is no presentation request, no DCQL query and no wallet invocation; the holder brings the token to you. See docs/WALLETS.md. Public and unauthenticated (the presentation is itself the capability) and strictly read-only.

Request body

application/jsonobject

Responses

200Verified presentation · object
400Not a valid Credda SD-JWT VC presentation (bad signature, expired, or a disclosure that does not match `_sd`) · Error

Example

curl -X POST https://api.credda.io/api/v1/verify/presentation \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /.well-known/openid-credential-issuer public

OID4VCI Credential Issuer Metadata

Credential Issuer Metadata per openid-4-verifiable-credential-issuance-1_0 (final, 2025-09-16). Describes the credential endpoint, the nonce endpoint, and every supported credential configuration, one per (credential type × format), DERIVED from the issuer's credential-type catalog so a new type cannot ship undiscoverable. Formats: `dc+sd-jwt` (SD-JWT VC, draft-ietf-oauth-sd-jwt-vc-17, selectively disclosable) and `jwt_vc_json` (W3C VC-JWT, all-or-nothing). `authorization_servers` is absent because this service is its own authorization server; its RFC 8414 metadata is at `/.well-known/oauth-authorization-server`. Public and unauthenticated: a wallet must be able to read it before it holds anything.

Responses

200Credential Issuer Metadata · object

Example

curl https://api.credda.io/.well-known/openid-credential-issuer
get /.well-known/oauth-authorization-server public

OAuth 2.0 Authorization Server Metadata (RFC 8414)

Token endpoint and supported grants for the wallet issuance flow. Only the Pre-Authorized Code grant is supported, and anonymous token requests are permitted (`pre-authorized_grant_anonymous_access_supported`) because a wallet holds no client credentials with Credda. `response_types_supported` is omitted, which OID4VCI §12.3 explicitly allows for a pre-authorized-code-only authorization server.

Responses

200Authorization Server Metadata · object

Example

curl https://api.credda.io/.well-known/oauth-authorization-server
post /oid4vci/token public

Exchange a pre-authorized code for an access token

OID4VCI §6. Form-encoded (or JSON) Token Request with `grant_type=urn:ietf:params:oauth:grant-type:pre-authorized_code` and the `pre-authorized_code` from the Credential Offer. The code is **single use and short lived**; a replay returns `invalid_grant`. Error bodies are OAuth-shaped (`{error, error_description}`), not Credda-shaped, because that is what a wallet parses.

Responses

200Access token · object
400OAuth error (invalid_request / invalid_grant / unsupported_grant_type) · OAuthError
503Issuance store unavailable, retry · OAuthError

Example

curl -X POST https://api.credda.io/oid4vci/token \
  -H "Content-Type: application/json" \
  -d '{ ... }'
post /oid4vci/nonce public

Get a fresh c_nonce for a key proof

OID4VCI §7. Returns a single-use `c_nonce` the wallet must put in its key-proof JWT, which is what makes the proof non-replayable. Unprotected by design: the nonce is a freshness challenge, not a capability.

Responses

200Nonce · object
503Issuance store unavailable, retry · OAuthError

Example

curl -X POST https://api.credda.io/oid4vci/nonce
post /oid4vci/credential public

Issue the credential into the wallet

OID4VCI §8. Bearer-authenticated with the access token from `/oid4vci/token`. Requires a `proofs.jwt` key proof (`typ: openid4vci-proof+jwt`, `aud` = the Credential Issuer Identifier, a fresh `nonce`, and the public key inline as a `jwk` header; `kid`/`x5c` are refused rather than pretended-to-be-checked). The issued credential is bound to that key via `cnf` and carries a StatusList2021 `credentialStatus`. Batch issuance is not supported (no `batch_credential_issuance` in metadata), so exactly one key proof is accepted. INVARIANT: issuance re-encodes what the deterministic engine already computed. No score is computed, read-through, or adjusted here.

Request body

application/jsonobject

Responses

200Credential · object
400OID4VCI credential error (invalid_proof / invalid_nonce / unknown_credential_configuration / credential_request_denied) · OAuthError
401invalid_token · OAuthError
503Issuance store unavailable, retry · OAuthError

Example

curl -X POST https://api.credda.io/oid4vci/credential \
  -H "Content-Type: application/json" \
  -d '{ ... }'

Open Badges

Open Badges 3.0 (1EdTech, document version 1.4.5): the closed, publicly readable set of achievements this issuer signs, plus the operator-only endpoint that attaches the proof. A badge records an outcome already on the ledger; it is never a rating or a recommendation, and nothing here can move a score.

get /api/v1/open-badges/achievements public

The closed set of Open Badges 3.0 achievements this issuer will sign

Public. Every signed credential's `achievement.id` resolves under this path, so a verifier reads the criteria from the issuer rather than from the document being checked. An achievement outside this set is refused, never signed.

Responses

200Achievement definitions · object

Example

curl https://api.credda.io/api/v1/open-badges/achievements
get /api/v1/open-badges/achievements/{badgeId} public

One achievement definition

Parameters

badgeId (path) required early-adopter | first-delivery | ten-strong | century-club | verified-professional | multi-platform | flawless-ten | one-year-reliable | identity-verified

Responses

200Achievement · object
404Unknown achievement · Error

Example

curl https://api.credda.io/api/v1/open-badges/achievements/{badgeId}
post /api/v1/admin/open-badges admin secret

Sign Open Badges 3.0 credentials for an anchored subject

Operator-only. Attaches Credda's Ed25519 proof (VC-JWT, VC Data Model 2.0 envelope) to badges the badge engine already awarded. The caller chooses only WHICH allowlisted achievement, for WHICH share-token-anchored subject, on WHICH date: the achievement name, description and criteria narrative that get signed come from the issuer, never from the request. Every credential carries the subject's StatusList2021 entry, so revoking the share token revokes the badge. Issues no events and touches no score.

Request body

application/jsonobject

Responses

201Signed Open Badges collection · object
400Unknown achievement id, or an invalid/future earnedAt · Error
403Test-mode subject · Error
404Unknown or revoked trust token · Error

Example

curl -X POST https://api.credda.io/api/v1/admin/open-badges \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ ... }'

Admin

Operator-only provisioning (X-Admin-Secret).

get /api/v1/admin/platforms admin secret

List platforms (+ optional consolidated usage)

Cursor-paginated list of platforms with metadata (tier, active flag, key count, child count, per-minute rate limit). Pass `referredBy=<platformId>` to list only the platforms a given PARENT provisioned, the reseller/ISV consolidated view (see docs/PARTNER_PLATFORM.md). Pass `usage=1` (optionally with `days`, or `from`+`to`, same window semantics as GET /api/v1/usage) to attach each platform's usage summary for the window and an aggregate `totals` across the returned page. Read-only observability; opens no new trust boundary (admin can already read any single platform), and nothing here reads or writes a score.

Parameters

cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.
limit (query) integer
referredBy (query) string: Filter to platforms provisioned by this parent platform id.
usage (query) 1 | true: Attach per-platform usage summaries + an aggregate over the page.
days (query) integer: Usage window (trailing days). Only read when `usage` is set.

Responses

200Platforms · object

Example

curl https://api.credda.io/api/v1/admin/platforms \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET"
post /api/v1/admin/platforms admin secret

Create a platform

Request body

application/jsonobject

Responses

201Created
400referredByPlatformId does not reference an existing platform · Error

Example

curl -X POST https://api.credda.io/api/v1/admin/platforms \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/admin/platforms/{id}/keys admin secret

List a platform's API keys (metadata only)

Key metadata: id, label, scopes, isActive, expiresAt, lastUsedAt, createdAt. Never a hash, never plaintext. Lets a caller that provisions keys on a user's behalf resolve the key id it needs for rotation.

Parameters

id (path) required string

Responses

200API keys · object
404Platform not found · Error

Example

curl https://api.credda.io/api/v1/admin/platforms/{id}/keys \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET"
post /api/v1/admin/platforms/{id}/keys admin secret

Generate an API key (shown once)

Optional `scopes` (defaults to full access), `expiresInDays` (1–3650; omitted = never expires) and `isTest` (default false; `true` issues a sandbox `crd_test_` key that can only ever create/read the platform's TEST data; see the Test mode section). An expired key is rejected at authentication like a revoked one. The response includes the key record `id` (usable with the rotate endpoint) alongside the plaintext `apiKey`. Rotation preserves a key's mode.

Parameters

id (path) required string

Request body

application/jsonobject

Responses

201API key

Example

curl -X POST https://api.credda.io/api/v1/admin/platforms/{id}/keys \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ ... }'
patch /api/v1/admin/platforms/{id}/keys/{keyId} admin secret

Re-scope an already-issued API key in place

Replaces a live key's `scopes` WITHOUT changing the credential; the key string, its mode (`isTest`), its label and its expiry are all untouched, so a deployed integration keeps working across the change. Built for the subscription plan change: scopes are otherwise frozen at issue time (create sets them, rotate copies them forward), so a caller that provisions keys against a plan could move a platform's trust tier on an upgrade or downgrade but never the scopes with it, leaving an upgraded customer with 403 SCOPE_INSUFFICIENT on the capability they just paid for, and a downgraded one holding the higher tier's write scopes indefinitely. `scopes` is required and must contain at least one entry: an EMPTY scope array means "unscoped = full access" at enforcement time, so accepting `[]` would turn a downgrade into a privilege escalation. To remove all access, deactivate the platform instead. Emits an `API_KEY_SCOPES_UPDATED` audit event carrying the before and after sets. Touches no score.

Parameters

id (path) required string
keyId (path) required string

Request body

application/jsonobject

Responses

200Updated key metadata (never a hash, never plaintext) · object
400Missing or invalid scopes (an empty array is refused) · Error
404Platform or key not found · Error

Example

curl -X PATCH https://api.credda.io/api/v1/admin/platforms/{id}/keys/{keyId} \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ ... }'
post /api/v1/admin/platforms/{id}/keys/{keyId}/rotate admin secret

Rotate an API key with a grace overlap (new key shown once)

Atomically creates a replacement key with the SAME label and scopes (plaintext returned exactly once) and sets the old key's `expiresAt` to now + `graceHours` (default 24, clamped 0–168), so both keys work during the grace window and a deployed integration can swap credentials without a hard cut-over. `graceHours: 0` kills the old key immediately. Rotation never extends the old key's life; a pre-existing earlier expiry wins. Emits an `API_KEY_ROTATED` audit event.

Parameters

id (path) required string
keyId (path) required string

Request body

application/jsonobject

Responses

201New key (plaintext shown once) + old-key grace deadline · object
400Key already revoked or expired · Error
404Platform or key not found · Error

Example

curl -X POST https://api.credda.io/api/v1/admin/platforms/{id}/keys/{keyId}/rotate \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ ... }'
patch /api/v1/admin/platforms/{id} admin secret

Update trust tier / deactivate / velocity-exempt

Parameters

id (path) required string

Request body

application/jsonobject

Responses

200Updated

Example

curl -X PATCH https://api.credda.io/api/v1/admin/platforms/{id} \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ ... }'
post /api/v1/admin/users/{id}/resolve-velocity-flag admin secret

Clear a velocity flag and recompute

Parameters

id (path) required string

Responses

200Cleared

Example

curl -X POST https://api.credda.io/api/v1/admin/users/{id}/resolve-velocity-flag \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET"
get /api/v1/admin/users/{id}/audit-log admin secret

Paginated audit trail

Parameters

id (path) required string

Responses

200Audit log

Example

curl https://api.credda.io/api/v1/admin/users/{id}/audit-log \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET"
delete /api/v1/admin/users/{id}/share-token admin secret

Withdraw a subject's public trust surface (admin mirror)

Operator-authenticated mirror of DELETE /api/v1/users/{id}/share-token, scoped by the externalId in the URL instead of the platform's own key, for callers that provision keys on a user's behalf and never see the plaintext. Built for cross-service ACCOUNT ERASURE (GDPR Art. 17): the account record lives in the product backend, but the published share token and the credentials issued against it live here. One call does both halves: every public surface that resolves a subject BY share token (verify page, credential, trust export, delivery receipts, OID4VCI issuance) stops resolving immediately, and the subject's StatusList2021 bit at /api/v1/status/revocation flips to revoked, so credentials already in a third party's hands verify as revoked offline. Idempotent: a subject with no token returns 200 with `alreadyRevoked: true`, so a failed revocation can be retried blindly. The response never contains the token. The append-only event ledger is deliberately untouched (its events are also the counterparty's record), and nothing here reads or writes a score.

Parameters

id (path) required string

Responses

200Revocation outcome · object
404Unknown externalId (USER_NOT_FOUND) · Error

Example

curl -X DELETE https://api.credda.io/api/v1/admin/users/{id}/share-token \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET"
get /api/v1/admin/platforms/{id}/webhooks admin secret

List a platform's webhooks

Parameters

id (path) required string

Responses

200Webhooks · object
404Platform not found · Error

Example

curl https://api.credda.io/api/v1/admin/platforms/{id}/webhooks \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET"
post /api/v1/admin/platforms/{id}/webhooks admin secret

Subscribe an HTTPS endpoint on behalf of a platform (secret returned once)

Parameters

id (path) required string

Request body

application/jsonobject

Responses

201Created · object
400Invalid url/events · Error
404Platform not found · Error

Example

curl -X POST https://api.credda.io/api/v1/admin/platforms/{id}/webhooks \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ ... }'
patch /api/v1/admin/platforms/{id}/webhooks/{webhookId} admin secret

Update url/events/description/isActive

Parameters

id (path) required string
webhookId (path) required string

Request body

application/jsonobject

Responses

200Updated · object
404Platform or webhook not found · Error

Example

curl -X PATCH https://api.credda.io/api/v1/admin/platforms/{id}/webhooks/{webhookId} \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ ... }'
delete /api/v1/admin/platforms/{id}/webhooks/{webhookId} admin secret

Delete a webhook

Parameters

id (path) required string
webhookId (path) required string

Responses

204Deleted
404Platform or webhook not found · Error

Example

curl -X DELETE https://api.credda.io/api/v1/admin/platforms/{id}/webhooks/{webhookId} \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET"
post /api/v1/admin/platforms/{id}/webhooks/{webhookId}/test admin secret

Send a synthetic signed delivery

Parameters

id (path) required string
webhookId (path) required string

Responses

200Delivery result · object
404Platform or webhook not found · Error

Example

curl -X POST https://api.credda.io/api/v1/admin/platforms/{id}/webhooks/{webhookId}/test \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET"
get /api/v1/admin/platforms/{id}/webhooks/{webhookId}/deliveries admin secret

Recent delivery attempts

Parameters

id (path) required string
webhookId (path) required string
cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.

Responses

200Deliveries · object
404Platform or webhook not found · Error

Example

curl https://api.credda.io/api/v1/admin/platforms/{id}/webhooks/{webhookId}/deliveries \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET"
post /api/v1/admin/platforms/{id}/webhooks/{webhookId}/deliveries/{deliveryId}/replay admin secret

Replay a past delivery (admin mirror)

Parameters

id (path) required string
webhookId (path) required string
deliveryId (path) required string

Responses

200Replay outcome · object
404Not found · Error
409Payload not retained · Error

Example

curl -X POST https://api.credda.io/api/v1/admin/platforms/{id}/webhooks/{webhookId}/deliveries/{deliveryId}/replay \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET"
get /api/v1/admin/platforms/{id}/monitors admin secret

List a platform's live score monitors

Admin mirror of GET /api/v1/monitors, scoped by platformId instead of the platform's own key, for callers that provision keys on a user's behalf and never see the plaintext. Manages LIVE monitors only; sandbox monitors are managed with the `crd_test_` key directly.

Parameters

id (path) required string
cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.
limit (query) integer

Responses

200Monitors · object
404Platform not found · Error

Example

curl https://api.credda.io/api/v1/admin/platforms/{id}/monitors \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET"
post /api/v1/admin/platforms/{id}/monitors admin secret

Create a score monitor on behalf of a platform

Admin mirror of POST /api/v1/monitors: identical schema, conditions and per-tier active-monitor cap (409 MONITOR_LIMIT_REACHED, `details` carries `{limit, tier}`).

Parameters

id (path) required string

Request body

application/jsonobject

Responses

201Created · object
400No condition provided / validation error · Error
404Platform not found, or unknown userId (USER_NOT_FOUND) · Error
409Active monitor limit reached for the plan tier (MONITOR_LIMIT_REACHED) · Error

Example

curl -X POST https://api.credda.io/api/v1/admin/platforms/{id}/monitors \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ ... }'
get /api/v1/admin/platforms/{id}/monitors/{monitorId} admin secret

Fetch one monitor (admin mirror)

Parameters

id (path) required string
monitorId (path) required string

Responses

200Monitor · object
404Platform or monitor not found · Error

Example

curl https://api.credda.io/api/v1/admin/platforms/{id}/monitors/{monitorId} \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET"
patch /api/v1/admin/platforms/{id}/monitors/{monitorId} admin secret

Update thresholds / onBandChange / isActive (admin mirror)

Set `belowScore`/`aboveScore` to null to clear a threshold. The updated monitor must keep at least one condition. Reactivation (`isActive: true`) re-checks the per-tier cap, exactly as the platform-authenticated route does.

Parameters

id (path) required string
monitorId (path) required string

Request body

application/jsonobject

Responses

200Updated · object
400Update would leave the monitor with no condition · Error
404Platform or monitor not found · Error
409Active monitor limit reached (MONITOR_LIMIT_REACHED) · Error

Example

curl -X PATCH https://api.credda.io/api/v1/admin/platforms/{id}/monitors/{monitorId} \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ ... }'
delete /api/v1/admin/platforms/{id}/monitors/{monitorId} admin secret

Delete a monitor (admin mirror)

Parameters

id (path) required string
monitorId (path) required string

Responses

204Deleted
404Platform or monitor not found · Error

Example

curl -X DELETE https://api.credda.io/api/v1/admin/platforms/{id}/monitors/{monitorId} \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET"
get /api/v1/admin/platforms/{id}/usage admin secret

A platform's usage vs. tier quota, on behalf of it

Same data and window/format semantics as GET /api/v1/usage (trailing `days` OR inclusive `from`/`to` range, optional `format=csv`), scoped by platformId instead of the platform's own key, for callers that provision keys on a user's behalf and never see the plaintext.

Parameters

id (path) required string
days (query) integer: Trailing window in days (default 7, max 400). Mutually exclusive with `from`/`to`.
from (query) string: Inclusive range start (ISO date, YYYY-MM-DD). Requires `to`; clamped to the 400-day history window (completed days beyond the 90-day live retention are served from durable daily rollups).
to (query) string: Inclusive range end (ISO date, YYYY-MM-DD). Requires `from`.
format (query) json | csv: Response format: 'json' (default) or 'csv' (flat statement, `Content-Disposition: attachment`).

Responses

200Usage summary (JSON by default; CSV statement when format=csv) · object
400Invalid window (USAGE_WINDOW_CONFLICT / USAGE_RANGE_INVALID / USAGE_FORMAT_INVALID) · Error
404Platform not found · Error

Example

curl https://api.credda.io/api/v1/admin/platforms/{id}/usage \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET"
get /api/v1/admin/platforms/{id}/activity admin secret

A platform's own activity log, on behalf of it

Admin mirror of GET /api/v1/activity, scoped by platformId instead of the platform's own key, for callers that provision keys on a user's behalf and never see the plaintext. Identical query semantics (`limit`/`cursor`/`action`/`from`/`to`) and, deliberately, an identical result set: the same payload-attribution filter, the same platform-visible action allowlist and the same columns. It cannot show more than the platform itself could see; audit actions without platform attribution remain operator-only via GET /admin/users/{id}/audit-log. Read-only; never affects a score.

Parameters

id (path) required string
cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.
limit (query) integer: Page size (1–100, default 25).
action (query) EVENT_CREATED | VELOCITY_FLAG_SET | PLATFORM_CREATED | PLATFORM_UPDATED | API_KEY_GENERATED | API_KEY_ROTATED | API_KEY_REVOKED | SHARE_TOKEN_MINTED | SHARE_TOKEN_REVOKED | CREDENTIAL_OFFER_CREATED | WEBHOOK_CREATED | WEBHOOK_UPDATED | WEBHOOK_DELETED | MONITOR_CREATED | MONITOR_UPDATED | MONITOR_DELETED | NOTIFICATION_CHANNEL_CREATED | NOTIFICATION_CHANNEL_DELETED | CONFIRMATION_REQUEST_CREATED | CONFIRMATION_REQUEST_CONFIRMED | CONFIRMATION_REQUEST_DECLINED | CONFIRMATION_REQUEST_CANCELLED | TEST_DATA_RESET | TEST_DATA_SEEDED: Filter to one audit action.
from (query) string: Inclusive lower bound on `createdAt` (ISO timestamp or YYYY-MM-DD).
to (query) string: Inclusive upper bound on `createdAt` (ISO timestamp or YYYY-MM-DD).

Responses

200Activity page · object
400Invalid query (ACTIVITY_ACTION_INVALID / ACTIVITY_RANGE_INVALID) · Error
404Platform not found · Error

Example

curl https://api.credda.io/api/v1/admin/platforms/{id}/activity \
  -H "X-Admin-Secret: $CREDDA_ADMIN_SECRET"

Platform

Programmatic self-administration: manage your OWN platform (identity, API keys, scopes) entirely via the API, no dashboard. Platform-key-authenticated and gated by the `admin` scope; every route is tenant-isolated and a minted key can never exceed the minting key. Nothing here touches a score, a plan, or a trust tier.

get /api/v1/platform public

Your platform identity, tier, rate limit, and the calling key's scopes

Read-only self-description so an integration can discover its own trust tier, per-minute rate limit, mode (live/test) and the scopes it is operating under, without a console. Reports the tier; it cannot change it (tier/plan changes are operator-only, by invariant). Requires the `admin` scope.

Responses

200Platform self-info · object
401Missing/invalid API key · Error
403API key lacks the admin scope · Error

Example

curl https://api.credda.io/api/v1/platform
get /api/v1/platform/keys public

List your own API keys (metadata only)

Key metadata for THIS platform only: id, label, scopes, isTest, isActive, expiresAt, lastUsedAt, createdAt. Never a hash, never plaintext. Requires the `admin` scope.

Responses

200API keys · object
401Missing/invalid API key · Error
403API key lacks the admin scope · Error

Example

curl https://api.credda.io/api/v1/platform/keys
post /api/v1/platform/keys public

Mint a new API key for your platform (shown once)

Create a key scoped AT OR BELOW the CALLING key: a key can never mint a child more powerful than itself (privilege ceiling). Omit `scopes` to inherit the calling key's scopes (never widened to `*`). A sandbox (test) key can only mint sandbox keys. `expiresInDays` 1–3650 (omitted = never expires). Emits an `API_KEY_GENERATED` audit event visible on GET /activity. Requires the `admin` scope.

Request body

application/jsonobject

Responses

201API key (plaintext shown once) · object
401Missing/invalid API key · Error
403Scope ceiling exceeded (SCOPE_CEILING_EXCEEDED), a test key minting a live key (TEST_MODE_NOT_ALLOWED), or the key lacks the admin scope · Error

Example

curl -X POST https://api.credda.io/api/v1/platform/keys \
  -H "Content-Type: application/json" \
  -d '{ ... }'
post /api/v1/platform/keys/{keyId}/rotate public

Rotate one of your keys with a grace overlap (new key shown once)

Atomically mint a replacement with the SAME label + scopes + mode and set the old key's expiry to now + graceHours (default 24, clamped 0–168), so both work during the window. graceHours:0 kills the old key at once. Rotation never extends a key's life. Scoped to your platform; a foreign keyId 404s. Emits API_KEY_ROTATED. Requires the `admin` scope.

Parameters

keyId (path) required string

Request body

application/jsonobject

Responses

201New key (plaintext shown once) + old-key grace deadline · object
400Key already revoked or expired · Error
401Missing/invalid API key · Error
403API key lacks the admin scope · Error
404Key not found (or belongs to another platform) · Error

Example

curl -X POST https://api.credda.io/api/v1/platform/keys/{keyId}/rotate \
  -H "Content-Type: application/json" \
  -d '{ ... }'
delete /api/v1/platform/keys/{keyId} public

Revoke one of your keys

Deactivate a key immediately (rejected at auth thereafter). Idempotent: revoking an already-revoked key returns 200 with alreadyRevoked:true. You may revoke the key you are currently holding (a compromised-key kill switch). Scoped to your platform; a foreign keyId 404s. Emits API_KEY_REVOKED. Requires the `admin` scope.

Parameters

keyId (path) required string

Responses

200Revocation outcome · object
401Missing/invalid API key · Error
403API key lacks the admin scope · Error
404Key not found (or belongs to another platform) · Error

Example

curl -X DELETE https://api.credda.io/api/v1/platform/keys/{keyId}

System

Health & readiness.

get /health public

Liveness

Responses

200OK

Example

curl https://api.credda.io/health
get /health/ready public

Readiness (checks DB, Redis, credentials)

Responses

200Ready
503Degraded

Example

curl https://api.credda.io/health/ready
get /status.json public

Public status: live health + latency, plus durable uptime history

Live dependency checks and in-process latency percentiles (reset on deploy), plus a durable `history` block that survives deploys: uptime % over 24h/7d and a bounded p95 series. History is SNAPSHOT-BASED: a snapshot is stored at most once per `STATUS_SNAPSHOT_INTERVAL_S` (default 300s) and uptime is the share of snapshots where both db and redis were healthy; it is not continuous probing. `history` is null if the durable layer is unreachable (fail-open). Backs the /status page.

Responses

200Current status · object

Example

curl https://api.credda.io/status.json

Plans

Developer plan catalog: tiers, scopes, rate limits, features.

get /api/v1/plans public

Developer plan catalog

The canonical catalog of developer tiers (Starter / Growth / Enterprise), each with its default key scopes, per-minute rate limit, continuous-monitor limit, official monthly price (`priceUsdMonthly`: Starter $49 / Growth $299 / Enterprise $1,500), support level and ordered feature matrix. This is the same data the API enforces (rate limits, scopes) and the pricing page renders, so the published tiers can never drift from what is enforced. Public and unauthenticated. Prices are official; self-serve checkout is not live yet; contact us to get started. INVARIANT: a plan governs API access only; no tier, and no amount of money, can move anyone's score.

Responses

200Plan catalog · object

Example

curl https://api.credda.io/api/v1/plans

Benchmarks

Aggregate cohort comparison: where a score falls relative to a population. Legitimate, ledger-derived cohorts only (never a protected class or demographic proxy); k-anonymity enforced (no statistic below the floor); deterministic and read-only. A distribution fact, never a rating, ranking, or verdict, and nothing here can move a score.

get /api/v1/users/{id}/benchmark public

Where this subject sits within a cohort (percentile)

The subject's percentile rank within a legitimate, ledger-derived cohort, plus the cohort's aggregate distribution for context. `?dimension=` (default `all`) selects which cohort; the subject's own record decides which cohort VALUE it belongs to. **K-anonymity is enforced**: if the cohort has fewer than the floor of subjects it comes back `available:false` (`reason: insufficient_data`) with NO percentile, never a comparison against a tiny group. `available:false` with `reason: no_score` means the subject has no computed score yet. This is the REAL comparison (an actual cross-user distribution), distinct from the deprecated `Score.percentile` (`100 − score`). A percentile is a distribution fact, never a verdict. Read-only; computes nothing that feeds back into a score.

Parameters

id (path) required string
dimension (query) all | subjectType | verificationDepthBand | activityVolumeBand | tenureBand: Cohort dimension. Default `all`.

Responses

200Subject benchmark (may be `available:false`) · object
400Unknown cohort dimension · Error
404User not found · Error

Example

curl https://api.credda.io/api/v1/users/{id}/benchmark
get /api/v1/benchmarks public

Benchmark catalog (cohort dimensions + k-anonymity floor)

Self-describes the aggregate cohort-comparison surface: the legitimate, LEDGER-DERIVED cohort dimensions (`all`, `subjectType`, `verificationDepthBand`, `activityVolumeBand`, `tenureBand`), the statistics returned, and the **k-anonymity floor** (`kAnonymity.minimumCohortSize`, env `BENCHMARK_MIN_COHORT`, default 20) below which nothing is disclosed. Public and unauthenticated: the same machine-readable source-of-truth shape as `/scoring/model` and `/enums`. Every cohort dimension is a fact about the RECORD, never a protected class or demographic proxy; the score exists to remove bias, and benchmarking must not reintroduce it. **A benchmark is a distribution fact (where a score falls relative to a population), never a rating, ranking, or verdict on a subject**; see the `disclosures` array. This is the real, distribution-based comparison; the legacy `Score.percentile` field (`100 − score`) is not a cross-user ranking and is deprecated.

Responses

200Benchmark catalog · object

Example

curl https://api.credda.io/api/v1/benchmarks
get /api/v1/benchmarks/distribution public

Aggregate score distribution for a cohort

Population statistics for a legitimate, ledger-derived cohort: `median`, `mean`, `p25`/`p75`/`p90`, and the per-band histogram. `?dimension=` (default `all`) selects the cohort dimension; `?cohort=` optionally narrows to one cohort value (omit it to get every cohort value on the dimension, each as its own distribution). **K-anonymity is enforced**: any cohort with fewer than the floor of subjects comes back `available:false` with NO numbers, and the sub-floor count is not disclosed. Raw minimum/maximum scores are never returned; only order statistics over the whole cohort and band-bucket counts. Aggregate-only: no other individual's score is ever returned. Deterministic and read-only; nothing here computes or writes a score. Keyed (`scores:read`) because it scans the population; a test key benchmarks only the test population.

Parameters

dimension (query) all | subjectType | verificationDepthBand | activityVolumeBand | tenureBand: Cohort dimension. Default `all`.
cohort (query) string: A single cohort value on the dimension. Omit for all values.

Responses

200Cohort distribution(s); some may be `available:false` under the k-anonymity floor · object
400Unknown cohort dimension · Error

Example

curl https://api.credda.io/api/v1/benchmarks/distribution

Reference

Machine-readable reference data: the error catalog and the wire enums, both derived from the constants the code enforces so they cannot drift.

get /api/v1/errors public

Machine-readable error catalog

Every stable error `code` the API can return, each with an HTTP status, a title, what happened, what to do about it, and a `retryable` flag. Also describes the error envelope (`{error, code, requestId, retryable, details?}`) and the retry contract. Public and unauthenticated. This is the SAME catalog the errors themselves are built from and that the `x-error-codes` extension is derived from, so a code can never be returned undocumented; the server will not compile if a route throws one that is missing here. Retry only codes with `retryable: true`, and honour `Retry-After` when present.

Responses

200Error catalog · object

Example

curl https://api.credda.io/api/v1/errors
get /api/v1/enums public

Self-describing enums

Every closed value set on the wire: `eventType`, `stakeLevel`, `scoreBand`, `disputeStatus`, `platformTier`, with a human description per value and the facts that matter (stake weights, band score floors, platform trust multipliers, and whether an event type may be reported directly). Public and unauthenticated. Derived from the Prisma enums, the zod ingestion schema and the formula constants the code actually enforces, so it cannot drift from what is validated. Use it to build pickers and validators without hard-coding the lists.

Responses

200Enum catalog · object

Example

curl https://api.credda.io/api/v1/enums
get /api/v1/outcome-templates public

Industry outcome templates: map real-world work to events

How a real-world business maps its work to Credda events. Covers professionals across many fields: trades (including licensed trades), home & personal care, clinical & healthcare, hospitality, retail, logistics, transportation & freight, warehouse & production, cleaning, landscaping, staffing & shift work, automotive repair, childcare, security, events & production, veterinary, professional/legal/finance/consulting services, education, creative & design, engineering, real estate, beauty & wellness, and software, and for each names the concrete outcomes that matter, the exact ingest `eventType` to report, a suggested stake, and, the load-bearing part, WHO the third-party witness is and how they confirm. The live catalog is the source of truth; query it for the current industry list. Each template also carries `witnessType` (which of four kinds of third party confirms it; descriptive only, never a weighting) and `timingBasis` (what `dueDate` and `completedAt` mean for that outcome, in the terms the business already uses). The catalog-level `fieldGuide` states the same mapping once per event field. This matters because on-time rate is a large share of the score and is derived from those two timestamps alone: an outcome reported without them can count as completed, never as completed **on time**. `notTheWorkersOutcome` names the real-world failure metrics that describe an action taken by somebody OTHER than the worker: a patient who did not attend, a delivery where nobody was home, a shift the business itself cancelled, weather, and must not be recorded as a negative event against a worker. Guidance, not enforcement: Credda cannot see why a booking was missed, which is why the boundary is stated rather than assumed. Guidance only, same shape as `/plans` and `/enums`: it writes nothing, reads nothing, and cannot touch a score. A template never sets verification; an outcome counts as verified only when the named witness confirms it. Every template maps to a real `INGEST_EVENT_TYPES` value (compile-time enforced). Public and unauthenticated. `?industry=` filters to one set.

Parameters

industry (query) string: Filter to one industry slug (e.g. `dental_clinical`, `trades_home_services`).

Responses

200Outcome template catalog · object

Example

curl https://api.credda.io/api/v1/outcome-templates
get /api/v1/changelog public

API version contract, changelog and deprecation policy

The version contract for `v1` as data, plus every dated change to this surface. Public and unauthenticated. `versioning` states exactly what **additive-only** guarantees: what can appear without notice (new endpoints, new response fields, new optional inputs, new enum values, new error codes, new webhook event types) and what would require a new major version (removals, renames, type or meaning changes, a newly-required field, a changed auth scheme). It also carries `componentVersions`: the scoring formula, reason codes, earnings, professional record, verified profile and benchmark versions, each imported from the module that owns it. `deprecations` lists anything scheduled for removal, with its replacement and sunset date; it is **empty**, because nothing in v1 has ever been deprecated. When something is, it is announced here at least 180 days ahead and the affected endpoint additionally answers with `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) headers. `entries` are newest-first, each with a date (the release date; this service deploys on merge), a category (`added` / `changed` / `deprecated` / `fixed` / `security`), a one-line summary and the endpoints it touches. A test validates every referenced endpoint against this OpenAPI document, so an entry cannot describe a route that does not exist.

Responses

200Version contract + changelog · object

Example

curl https://api.credda.io/api/v1/changelog
get /api/v1/reason-codes public

Adverse-action reason-code catalog

The stable, versioned meaning of every reason code the scoring explanation can attribute to a record, with a plain-language consumer-facing description per code, a `factor` and a `direction` (`adverse` / `supporting`). Public and unauthenticated. Built for **B2B2C partners** (e.g. a cash-advance app or payroll-API vendor) that must issue an ECOA / Regulation B (12 CFR 1002.9(b)(2)) statement of *specific* principal reasons when they take an adverse action. A subject's ranked reason codes are returned on `GET /api/v1/users/{id}/score/explain` (the additive `reasonCodes` object); the partner draws its statement of reasons from the `adverseActionReasons`, ranked by importance-weighted contribution, taking the top `keyFactorLimit` (the FCRA / Reg B four-reason convention; not hard-capped, so a real reason is never hidden to hit a number). Every code is DETERMINISTIC factor attribution over facts the pure formula already produces, reproducible from the append-only ledger, versioned by `reasonCodesVersion` so a code never changes meaning under a partner. **Credda is not a creditor and is not a consumer reporting agency; it renders no decision and issues no notice; the adverse action and its consumer notice belong solely to the partner.** See the `disclosures` array.

Responses

200Reason-code catalog · object

Example

curl https://api.credda.io/api/v1/reason-codes

Activity

Platform transparency: the audit trail of what your own keys and config did, plus own-data export. Read-only observability; never affects a score.

get /api/v1/activity public

Your own activity log (self-serve audit trail + CSV export)

Cursor-paginated, newest-first view of the audit-log rows produced by the authenticated platform's own keys and config: events reported, webhooks/monitors changed, share tokens minted, keys issued/rotated/revoked. Strictly scoped: only rows recorded with THIS platform's attribution are ever returned; audit actions without platform attribution remain operator-only. Optional `action` filter plus `from`/`to` ISO time bounds on `createdAt`. `format=csv` returns the same rows as an RFC-4180 file (Content-Disposition attachment) for SIEM / data-governance export; page a full export by looping on the `X-Next-Cursor` response header until it is absent. Read-only observability under the `usage` read scope (like GET /usage), never affects a score.

Parameters

cursor (query) string: Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.
limit (query) integer: Page size (1–100, default 25).
action (query) EVENT_CREATED | VELOCITY_FLAG_SET | PLATFORM_CREATED | PLATFORM_UPDATED | API_KEY_GENERATED | API_KEY_ROTATED | API_KEY_REVOKED | SHARE_TOKEN_MINTED | SHARE_TOKEN_REVOKED | CREDENTIAL_OFFER_CREATED | WEBHOOK_CREATED | WEBHOOK_UPDATED | WEBHOOK_DELETED | MONITOR_CREATED | MONITOR_UPDATED | MONITOR_DELETED | NOTIFICATION_CHANNEL_CREATED | NOTIFICATION_CHANNEL_DELETED | CONFIRMATION_REQUEST_CREATED | CONFIRMATION_REQUEST_CONFIRMED | CONFIRMATION_REQUEST_DECLINED | CONFIRMATION_REQUEST_CANCELLED | TEST_DATA_RESET | TEST_DATA_SEEDED: Filter to one audit action.
from (query) string: Inclusive lower bound on `createdAt` (ISO timestamp or YYYY-MM-DD).
to (query) string: Inclusive upper bound on `createdAt` (ISO timestamp or YYYY-MM-DD).
format (query) json | csv: Response format: `json` (default) or `csv`. CSV columns: id,createdAt,action,requestId,payload.

Responses

200Activity page (JSON) or CSV export (attachment; next page in the X-Next-Cursor header) · object
400Invalid query (ACTIVITY_ACTION_INVALID / ACTIVITY_RANGE_INVALID / ACTIVITY_FORMAT_INVALID) · Error
401Missing/invalid API key · Error
403API key lacks usage:read · Error

Example

curl https://api.credda.io/api/v1/activity

Test Mode

Sandbox utilities for crd_test_ keys. Test data lives in a per-platform namespace, never touches real trust, and is the only data in the product that can be deleted.

post /api/v1/test/seed public

Seed the sandbox with synthetic subjects (test keys only)

Populates the authenticated platform's TEST universe with 5 unmistakably synthetic subjects and their outcome events, so your first score read returns something legible instead of `404 User not found`. Requires a sandbox (`crd_test_`) key; a live key gets `403 TEST_MODE_ONLY` before anything is written. **No scope is required and no request body is needed:** the sandbox routes are gated by mode rather than by scope, because a sandbox key's only authority is over its own disposable test universe, and scopes are issued from the caller's plan (which would otherwise leave free-tier sandboxes unable to seed or reset themselves). Every seeded external id starts with `sbx_` and every seeded event carries `metadata.synthetic: true`, so seeded rows are identifiable from the ledger itself. Events are written through the SAME append-only path as `POST /events` (same velocity guard, same audit trail) and scores come from the identical deterministic formula, `calculateCreddaScore` remains the sole writer of a score snapshot. **Idempotent.** A subject that already has events is left untouched (`alreadySeeded: true`) rather than doubled; the ledger is append-only, so re-seeding would otherwise silently change a score. Use `DELETE /api/v1/test/data` to start over. Each subject is described by the SHAPE of its record, never by a promised band: the number depends on your own platform trust tier, exactly as it would in production.

Responses

201Seed summary, with each subject's freshly computed score · object
403TEST_MODE_ONLY, seeding requires a crd_test_ key · Error

Example

curl -X POST https://api.credda.io/api/v1/test/seed
delete /api/v1/test/data public

Reset the platform's test data (test keys only)

Deletes the authenticated platform's TEST universe: its test events, its test users (cascading their score snapshots, disputes and monitors) and its test screening jobs. Requires a sandbox (`crd_test_`) key; a live key gets `403 TEST_MODE_ONLY`. This is the ONLY deletion path in the product, acceptable only because every row it touches is test data by construction (every delete filter asserts the test flag, belt and braces). The live Event ledger remains append-only; no endpoint can delete real data. No scope is required: like `POST /api/v1/test/seed`, this route is gated by mode rather than by scope; the mode check is the stronger gate, and a scope check would only leave free-tier sandboxes unable to reset themselves.

Responses

200Reset summary · object
403TEST_MODE_ONLY, a live key can never delete anything · Error

Example

curl -X DELETE https://api.credda.io/api/v1/test/data