{"info":{"_postman_id":"credda-api","name":"Credda Reliability Score API (v1.0)","description":"Generated from https://api.credda.io/openapi.json; re-import that link any time to pick up new endpoints. Set the `apiKey` variable to a platform key from https://api.credda.io/console.","schema":"https://schema.getpostman.com/json/collection/v2.1.0/collection.json"},"variable":[{"key":"baseUrl","value":"https://api.credda.io"},{"key":"apiKey","value":""},{"key":"adminSecret","value":""}],"item":[{"name":"System","item":[{"name":"Liveness","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/health","host":["{{baseUrl}}"],"path":["health"]}}},{"name":"Readiness (checks DB, Redis, credentials)","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/health/ready","host":["{{baseUrl}}"],"path":["health","ready"]}}},{"name":"Public status: live health + latency, plus durable uptime history","request":{"method":"GET","description":"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.","header":[],"url":{"raw":"{{baseUrl}}/status.json","host":["{{baseUrl}}"],"path":["status.json"]}}}]},{"name":"Events","item":[{"name":"Ingest an outcome event","request":{"method":"POST","description":"Appends an immutable event and asynchronously recomputes the score. Idempotent via `Idempotency-Key`.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/events","host":["{{baseUrl}}"],"path":["api","v1","events"]},"body":{"mode":"raw","raw":"{\n  \"userId\": \"string\",\n  \"eventType\": \"TRANSACTION_COMPLETED\",\n  \"dueDate\": \"string\",\n  \"completedAt\": \"string\",\n  \"stakeLevel\": \"HIGH\",\n  \"isVerified\": true,\n  \"transactionValue\": 0,\n  \"metadata\": {}\n}","options":{"raw":{"language":"json"}}}}},{"name":"Ingest a batch of outcome events","request":{"method":"POST","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/events/batch","host":["{{baseUrl}}"],"path":["api","v1","events","batch"]},"body":{"mode":"raw","raw":"{\n  \"events\": []\n}","options":{"raw":{"language":"json"}}}}},{"name":"Export your own reported events (data portability)","request":{"method":"GET","description":"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`).","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/events/export","host":["{{baseUrl}}"],"path":["api","v1","events","export"]}}}]},{"name":"Webhooks","item":[{"name":"Real-time event stream (Server-Sent Events)","request":{"method":"GET","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/events/stream","host":["{{baseUrl}}"],"path":["api","v1","events","stream"]}}},{"name":"List your webhooks","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/webhooks","host":["{{baseUrl}}"],"path":["api","v1","webhooks"]}}},{"name":"Subscribe an HTTPS endpoint (secret returned once)","request":{"method":"POST","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/webhooks","host":["{{baseUrl}}"],"path":["api","v1","webhooks"]},"body":{"mode":"raw","raw":"{\n  \"url\": \"string\",\n  \"events\": [],\n  \"description\": \"string\"\n}","options":{"raw":{"language":"json"}}}}},{"name":"Fetch one webhook","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/webhooks/:id","host":["{{baseUrl}}"],"path":["api","v1","webhooks",":id"],"variable":[{"key":"id","value":""}]}}},{"name":"Update url/events/description/isActive","request":{"method":"PATCH","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/webhooks/:id","host":["{{baseUrl}}"],"path":["api","v1","webhooks",":id"],"variable":[{"key":"id","value":""}]}}},{"name":"Delete a webhook","request":{"method":"DELETE","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/webhooks/:id","host":["{{baseUrl}}"],"path":["api","v1","webhooks",":id"],"variable":[{"key":"id","value":""}]}}},{"name":"Send a synthetic signed delivery","request":{"method":"POST","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/webhooks/:id/test","host":["{{baseUrl}}"],"path":["api","v1","webhooks",":id","test"],"variable":[{"key":"id","value":""}]}}},{"name":"Recent outbound events across ALL your endpoints (sample data)","request":{"method":"GET","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/webhooks/deliveries","host":["{{baseUrl}}"],"path":["api","v1","webhooks","deliveries"]}}},{"name":"Recent delivery attempts","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/webhooks/:id/deliveries","host":["{{baseUrl}}"],"path":["api","v1","webhooks",":id","deliveries"],"variable":[{"key":"id","value":""}]}}},{"name":"Replay a past delivery","request":{"method":"POST","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/webhooks/:id/deliveries/:deliveryId/replay","host":["{{baseUrl}}"],"path":["api","v1","webhooks",":id","deliveries",":deliveryId","replay"],"variable":[{"key":"id","value":""},{"key":"deliveryId","value":""}]}}},{"name":"Outbound webhook event catalog","request":{"method":"GET","description":"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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/webhooks/events","host":["{{baseUrl}}"],"path":["api","v1","webhooks","events"]}}}]},{"name":"Activity","item":[{"name":"Your own activity log (self-serve audit trail + CSV export)","request":{"method":"GET","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/activity","host":["{{baseUrl}}"],"path":["api","v1","activity"]}}}]},{"name":"Platform","item":[{"name":"Your platform identity, tier, rate limit, and the calling key's scopes","request":{"method":"GET","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/platform","host":["{{baseUrl}}"],"path":["api","v1","platform"]}}},{"name":"List your own API keys (metadata only)","request":{"method":"GET","description":"Key metadata for THIS platform only: id, label, scopes, isTest, isActive, expiresAt, lastUsedAt, createdAt. Never a hash, never plaintext. Requires the `admin` scope.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/platform/keys","host":["{{baseUrl}}"],"path":["api","v1","platform","keys"]}}},{"name":"Mint a new API key for your platform (shown once)","request":{"method":"POST","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/platform/keys","host":["{{baseUrl}}"],"path":["api","v1","platform","keys"]},"body":{"mode":"raw","raw":"{\n  \"label\": \"string\",\n  \"scopes\": [],\n  \"expiresInDays\": 0,\n  \"isTest\": true\n}","options":{"raw":{"language":"json"}}}}},{"name":"Rotate one of your keys with a grace overlap (new key shown once)","request":{"method":"POST","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/platform/keys/:keyId/rotate","host":["{{baseUrl}}"],"path":["api","v1","platform","keys",":keyId","rotate"],"variable":[{"key":"keyId","value":""}]},"body":{"mode":"raw","raw":"{\n  \"graceHours\": 0\n}","options":{"raw":{"language":"json"}}}}},{"name":"Revoke one of your keys","request":{"method":"DELETE","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/platform/keys/:keyId","host":["{{baseUrl}}"],"path":["api","v1","platform","keys",":keyId"],"variable":[{"key":"keyId","value":""}]}}}]},{"name":"Scores","item":[{"name":"List YOUR OWN subjects (query + export your book of business)","request":{"method":"GET","description":"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`).\n\n**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).\n\n**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.\n\n**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.\n\nDefault `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.\n\nRead-only under `scores:read`; nothing here computes or writes a score.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users","host":["{{baseUrl}}"],"path":["api","v1","users"]}}},{"name":"Size and shape a segment of your book (counts, not rows)","request":{"method":"GET","description":"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.\n\nTakes 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.\n\n**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.\n\n**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.\n\nRead-only under `scores:read`; nothing here computes or writes a score. These are counts over your own records, never a verdict on any subject.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/summary","host":["{{baseUrl}}"],"path":["api","v1","users","summary"]}}},{"name":"Batch score read (up to 100 users in one call)","request":{"method":"POST","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/users/scores","host":["{{baseUrl}}"],"path":["api","v1","users","scores"]},"body":{"mode":"raw","raw":"{\n  \"userIds\": []\n}","options":{"raw":{"language":"json"}}}}},{"name":"Current score","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/score","host":["{{baseUrl}}"],"path":["api","v1","users",":id","score"],"variable":[{"key":"id","value":""}]}}},{"name":"Score snapshot history","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/score/history","host":["{{baseUrl}}"],"path":["api","v1","users",":id","score","history"],"variable":[{"key":"id","value":""}]}}},{"name":"Plain-language score breakdown (with adverse-action reason codes)","request":{"method":"GET","description":"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`.**","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/score/explain","host":["{{baseUrl}}"],"path":["api","v1","users",":id","score","explain"],"variable":[{"key":"id","value":""}]}}},{"name":"Reliability at dispatch: the compact hot-path record read","request":{"method":"GET","description":"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.\n\n**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`.\n\n**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.\n\n`context` defaults to `dispatch`, the only context served today; an unrecognised value is a 400 (`DISPATCH_CONTEXT_INVALID`) rather than a silently different projection.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/reliability","host":["{{baseUrl}}"],"path":["api","v1","users",":id","reliability"],"variable":[{"key":"id","value":""}]}}},{"name":"Evidence-based trust explanation (summary, strengths, risks)","request":{"method":"GET","description":"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).","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/trust-summary","host":["{{baseUrl}}"],"path":["api","v1","users",":id","trust-summary"],"variable":[{"key":"id","value":""}]}}},{"name":"Factor-level explanation of the last score change","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/score/delta","host":["{{baseUrl}}"],"path":["api","v1","users",":id","score","delta"],"variable":[{"key":"id","value":""}]}}},{"name":"Modular score components (Reliability, Timeliness, Trustworthiness, Verification Confidence, Consistency, Momentum)","request":{"method":"GET","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/score/components","host":["{{baseUrl}}"],"path":["api","v1","users",":id","score","components"],"variable":[{"key":"id","value":""}]}}},{"name":"Unified chronological feed of events + score changes","request":{"method":"GET","description":"Merges the Event ledger and ScoreSnapshot history into one cursor-paginated, newest-first feed. Read-only: a view, not a new source of truth.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/timeline","host":["{{baseUrl}}"],"path":["api","v1","users",":id","timeline"],"variable":[{"key":"id","value":""}]}}},{"name":"What-if score projection (read-only, never writes a snapshot)","request":{"method":"POST","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/score/project","host":["{{baseUrl}}"],"path":["api","v1","users",":id","score","project"],"variable":[{"key":"id","value":""}]},"body":{"mode":"raw","raw":"{\n  \"events\": []\n}","options":{"raw":{"language":"json"}}}}},{"name":"Résumé / document advisory: who confirms it, and how it affects the score (writes nothing)","request":{"method":"POST","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/documents/analyze","host":["{{baseUrl}}"],"path":["api","v1","users",":id","documents","analyze"],"variable":[{"key":"id","value":""}]},"body":{"mode":"raw","raw":"{\n  \"claims\": []\n}","options":{"raw":{"language":"json"}}}}},{"name":"Contributing platforms","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/platforms","host":["{{baseUrl}}"],"path":["api","v1","users",":id","platforms"],"variable":[{"key":"id","value":""}]}}},{"name":"Advisory anti-gaming risk signals (never affects the score)","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/risk","host":["{{baseUrl}}"],"path":["api","v1","users",":id","risk"],"variable":[{"key":"id","value":""}]}}},{"name":"Your API usage vs. tier quota","request":{"method":"GET","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/usage","host":["{{baseUrl}}"],"path":["api","v1","usage"]}}},{"name":"Quota transparency: remaining monthly calls + reset","request":{"method":"GET","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/usage/quota","host":["{{baseUrl}}"],"path":["api","v1","usage","quota"]}}},{"name":"Usage as metered-billing meters (Stripe Billing Meters shape)","request":{"method":"GET","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/usage/meters","host":["{{baseUrl}}"],"path":["api","v1","usage","meters"]}}},{"name":"The scoring model, as data","request":{"method":"GET","description":"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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/scoring/model","host":["{{baseUrl}}"],"path":["api","v1","scoring","model"]}}}]},{"name":"Earnings","item":[{"name":"Verified earnings attestation (monthly breakdown + stability metrics)","request":{"method":"GET","description":"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).\n\n**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.\n\n**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.\n\nPure derivation over the same ledger the score reads: this surface reads no score, writes nothing, and cannot influence a score.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/earnings","host":["{{baseUrl}}"],"path":["api","v1","users",":id","earnings"],"variable":[{"key":"id","value":""}]}}},{"name":"Compact earnings summary (trailing 12m total, median monthly, volatility, coverage)","request":{"method":"GET","description":"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`.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/earnings/summary","host":["{{baseUrl}}"],"path":["api","v1","users",":id","earnings","summary"],"variable":[{"key":"id","value":""}]}}},{"name":"Mint a signed Verified Earnings Credential (W3C VC-JWT)","request":{"method":"POST","description":"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.\n\nUses 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.\n\nWrites nothing score-side. The credential attests recorded outcomes; it is not an income verification for a credit decision and not a consumer report.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/earnings/credential","host":["{{baseUrl}}"],"path":["api","v1","users",":id","earnings","credential"],"variable":[{"key":"id","value":""}]},"body":{"mode":"raw","raw":"{\n  \"months\": 0,\n  \"from\": \"string\",\n  \"to\": \"string\",\n  \"ttlSeconds\": 0\n}","options":{"raw":{"language":"json"}}}}}]},{"name":"Professional Record","item":[{"name":"Worker-owned summary of a verified work record (résumé-shaped)","request":{"method":"GET","description":"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.\n\n**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.\n\n**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.**\n\nPure derivation over the same ledger the score reads: this surface reads no score, writes nothing, and cannot influence a score.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/professional-record","host":["{{baseUrl}}"],"path":["api","v1","users",":id","professional-record"],"variable":[{"key":"id","value":""}]}}},{"name":"Mint a signed Professional Record Credential + an \"Add to LinkedIn\" link","request":{"method":"POST","description":"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.\n\nAlso 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.\n\nUses 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.\n\nWrites nothing score-side. It attests a record; it is not a hiring recommendation and not a consumer report.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/professional-record/credential","host":["{{baseUrl}}"],"path":["api","v1","users",":id","professional-record","credential"],"variable":[{"key":"id","value":""}]},"body":{"mode":"raw","raw":"{\n  \"ttlSeconds\": 0\n}","options":{"raw":{"language":"json"}}}}},{"name":"Portable professional record in the open JSON Resume schema","request":{"method":"GET","description":"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.\n\n**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.\n\nThe 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.\n\n**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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/career-export","host":["{{baseUrl}}"],"path":["api","v1","users",":id","career-export"],"variable":[{"key":"id","value":""}]}}},{"name":"One-call worker reliability report (decision-support dossier)","request":{"method":"GET","description":"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:\n\n- `reliability`: score, band, verified-evidence confidence, and the `formulaVersion` / `reasonCodesVersion` that produced them.\n- `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.\n- `verifiedExperience`: verified vs total outcomes, `verificationDepth`, verified-platform breadth, and `tenure` (the observed span of the record).\n- `topFactors`: the ranked reason-code drivers (WHY the score is what it is), merged across adverse/supporting and ordered by contribution.\n- `recentOutcomes`: the last N confirmed outcomes, **each flagged `verified` vs `self_reported`** so self-reported activity is never presented as verified.\n- `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.\n\n**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.\n\nRead-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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/reliability-report","host":["{{baseUrl}}"],"path":["api","v1","users",":id","reliability-report"],"variable":[{"key":"id","value":""}]}}},{"name":"List the subject's personal projects (verified vs self-reported)","request":{"method":"GET","description":"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`.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/projects","host":["{{baseUrl}}"],"path":["api","v1","users",":id","projects"],"variable":[{"key":"id","value":""}]}}},{"name":"Record a personal project (self-reported by default)","request":{"method":"POST","description":"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.\n\nThe `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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/projects","host":["{{baseUrl}}"],"path":["api","v1","users",":id","projects"],"variable":[{"key":"id","value":""}]},"body":{"mode":"raw","raw":"{\n  \"title\": \"string\",\n  \"url\": \"string\",\n  \"description\": \"string\",\n  \"verifiedBy\": \"string\"\n}","options":{"raw":{"language":"json"}}}}}]},{"name":"Verified Profile","item":[{"name":"Verified-profile measure (verification depth of the qualifications record)","request":{"method":"GET","description":"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).\n\n**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.\n\nComplements `GET /api/v1/users/{id}/professional-record`, which summarises the RELIABILITY record (outcome events and tenure). Different populations, different endpoints.\n\n**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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/verified-profile","host":["{{baseUrl}}"],"path":["api","v1","users",":id","verified-profile"],"variable":[{"key":"id","value":""}]}}},{"name":"Record a qualification claim (education / skill / certification / employment)","request":{"method":"POST","description":"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.\n\nThere 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.\n\n`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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/qualifications","host":["{{baseUrl}}"],"path":["api","v1","users",":id","qualifications"],"variable":[{"key":"id","value":""}]},"body":{"mode":"raw","raw":"{\n  \"category\": \"education\",\n  \"label\": \"string\",\n  \"issuer\": \"string\",\n  \"jurisdiction\": \"US-TX\",\n  \"reference\": \"TECL-28417\",\n  \"verifiedBy\": \"string\"\n}","options":{"raw":{"language":"json"}}}}},{"name":"Bulk-import a claimed professional record (education / employment / skills / certifications)","request":{"method":"POST","description":"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).\n\nUp 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.\n\nPartial-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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/qualifications/import","host":["{{baseUrl}}"],"path":["api","v1","users",":id","qualifications","import"],"variable":[{"key":"id","value":""}]},"body":{"mode":"raw","raw":"{\n  \"claims\": []\n}","options":{"raw":{"language":"json"}}}}}]},{"name":"Wallets","item":[{"name":"Mint an OID4VCI Credential Offer for this subject","request":{"method":"POST","description":"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.\n\nThe 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.\n\n`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.\n\nUses 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.\n\nINVARIANT: no score is computed or adjusted here.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/credential-offer","host":["{{baseUrl}}"],"path":["api","v1","users",":id","credential-offer"],"variable":[{"key":"id","value":""}]},"body":{"mode":"raw","raw":"{\n  \"credentialConfigurationIds\": [],\n  \"scope\": \"minimal\"\n}","options":{"raw":{"language":"json"}}}}},{"name":"Verify an SD-JWT VC presentation a holder handed you","request":{"method":"POST","description":"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.\n\nReturns 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.\n\nThis 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.\n\nPublic and unauthenticated (the presentation is itself the capability) and strictly read-only.","header":[{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/verify/presentation","host":["{{baseUrl}}"],"path":["api","v1","verify","presentation"]},"body":{"mode":"raw","raw":"{\n  \"vp_token\": \"string\",\n  \"presentation\": \"string\"\n}","options":{"raw":{"language":"json"}}}}},{"name":"OID4VCI Credential Issuer Metadata","request":{"method":"GET","description":"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.\n\nFormats: `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`.\n\nPublic and unauthenticated: a wallet must be able to read it before it holds anything.","header":[],"url":{"raw":"{{baseUrl}}/.well-known/openid-credential-issuer","host":["{{baseUrl}}"],"path":[".well-known","openid-credential-issuer"]}}},{"name":"OAuth 2.0 Authorization Server Metadata (RFC 8414)","request":{"method":"GET","description":"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.","header":[],"url":{"raw":"{{baseUrl}}/.well-known/oauth-authorization-server","host":["{{baseUrl}}"],"path":[".well-known","oauth-authorization-server"]}}},{"name":"Exchange a pre-authorized code for an access token","request":{"method":"POST","description":"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.","header":[],"url":{"raw":"{{baseUrl}}/oid4vci/token","host":["{{baseUrl}}"],"path":["oid4vci","token"]}}},{"name":"Get a fresh c_nonce for a key proof","request":{"method":"POST","description":"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.","header":[],"url":{"raw":"{{baseUrl}}/oid4vci/nonce","host":["{{baseUrl}}"],"path":["oid4vci","nonce"]}}},{"name":"Issue the credential into the wallet","request":{"method":"POST","description":"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`.\n\nBatch issuance is not supported (no `batch_credential_issuance` in metadata), so exactly one key proof is accepted.\n\nINVARIANT: issuance re-encodes what the deterministic engine already computed. No score is computed, read-through, or adjusted here.","header":[{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/oid4vci/credential","host":["{{baseUrl}}"],"path":["oid4vci","credential"]},"body":{"mode":"raw","raw":"{\n  \"credential_configuration_id\": \"string\",\n  \"proofs\": {}\n}","options":{"raw":{"language":"json"}}}}}]},{"name":"Benchmarks","item":[{"name":"Where this subject sits within a cohort (percentile)","request":{"method":"GET","description":"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.\n\n**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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/benchmark","host":["{{baseUrl}}"],"path":["api","v1","users",":id","benchmark"],"variable":[{"key":"id","value":""}]}}},{"name":"Benchmark catalog (cohort dimensions + k-anonymity floor)","request":{"method":"GET","description":"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`.\n\nEvery 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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/benchmarks","host":["{{baseUrl}}"],"path":["api","v1","benchmarks"]}}},{"name":"Aggregate score distribution for a cohort","request":{"method":"GET","description":"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).\n\n**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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/benchmarks/distribution","host":["{{baseUrl}}"],"path":["api","v1","benchmarks","distribution"]}}}]},{"name":"Verify","item":[{"name":"Mint/rotate a public trust-badge token","request":{"method":"POST","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/share-token","host":["{{baseUrl}}"],"path":["api","v1","users",":id","share-token"],"variable":[{"key":"id","value":""}]}}},{"name":"Revoke the share token","request":{"method":"DELETE","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/users/:id/share-token","host":["{{baseUrl}}"],"path":["api","v1","users",":id","share-token"],"variable":[{"key":"id","value":""}]}}},{"name":"Resolve a share token to a PII-free payload + credential","request":{"method":"GET","description":"Public trust payload for the share token, reduced to the requested disclosure scope, plus a signed credential over exactly those disclosed facts.\n\n`?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.\n\n`?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.\n\n`?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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/verify/:token","host":["{{baseUrl}}"],"path":["api","v1","verify",":token"],"variable":[{"key":"token","value":""}]}}},{"name":"Signed credential (?format=w3c → W3C VC-JWT)","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/api/v1/verify/:token/credential","host":["{{baseUrl}}"],"path":["api","v1","verify",":token","credential"],"variable":[{"key":"token","value":""}]}}},{"name":"Portable, self-verifying trust export","request":{"method":"GET","description":"A single bundle the subject owns: current public score + a signed W3C credential (offline-verifiable) + a revocation pointer. Public: the token is the capability.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/verify/:token/export","host":["{{baseUrl}}"],"path":["api","v1","verify",":token","export"],"variable":[{"key":"token","value":""}]}}},{"name":"Portable professional record in an open résumé schema","request":{"method":"GET","description":"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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/verify/:token/career-export","host":["{{baseUrl}}"],"path":["api","v1","verify",":token","career-export"],"variable":[{"key":"token","value":""}]}}},{"name":"Worker-consent reliability report","request":{"method":"GET","description":"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.\n\nFail-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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/verify/:token/reliability-report","host":["{{baseUrl}}"],"path":["api","v1","verify",":token","reliability-report"],"variable":[{"key":"token","value":""}]}}},{"name":"StatusList2021 revocation list (signed VC)","request":{"method":"GET","description":"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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/status/revocation","host":["{{baseUrl}}"],"path":["api","v1","status","revocation"]}}}]},{"name":"Analytics","item":[{"name":"Event volume time-series (by day + by type + verified & confirmed share)","request":{"method":"GET","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/analytics/events","host":["{{baseUrl}}"],"path":["api","v1","analytics","events"]}}},{"name":"Score distribution + movement across your subjects","request":{"method":"GET","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/analytics/scores","host":["{{baseUrl}}"],"path":["api","v1","analytics","scores"]}}}]},{"name":"Disputes","item":[{"name":"Resolve a dispute","request":{"method":"PATCH","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/disputes/:id/resolve","host":["{{baseUrl}}"],"path":["api","v1","disputes",":id","resolve"],"variable":[{"key":"id","value":""}]},"body":{"mode":"raw","raw":"{\n  \"outcome\": \"FOR_USER\"\n}","options":{"raw":{"language":"json"}}}}}]},{"name":"Monitors","item":[{"name":"List your score monitors","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/monitors","host":["{{baseUrl}}"],"path":["api","v1","monitors"]}}},{"name":"Create a score monitor on one of your users","request":{"method":"POST","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/monitors","host":["{{baseUrl}}"],"path":["api","v1","monitors"]},"body":{"mode":"raw","raw":"{\n  \"userId\": \"string\",\n  \"belowScore\": 0,\n  \"aboveScore\": 0,\n  \"onBandChange\": true\n}","options":{"raw":{"language":"json"}}}}},{"name":"Fetch one monitor","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/monitors/:id","host":["{{baseUrl}}"],"path":["api","v1","monitors",":id"],"variable":[{"key":"id","value":""}]}}},{"name":"Update thresholds / onBandChange / isActive","request":{"method":"PATCH","description":"Set `belowScore`/`aboveScore` to null to clear a threshold. The updated monitor must keep at least one condition.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/monitors/:id","host":["{{baseUrl}}"],"path":["api","v1","monitors",":id"],"variable":[{"key":"id","value":""}]},"body":{"mode":"raw","raw":"{\n  \"belowScore\": 0,\n  \"aboveScore\": 0,\n  \"onBandChange\": true,\n  \"isActive\": true\n}","options":{"raw":{"language":"json"}}}}},{"name":"Delete a monitor (hard delete; it is config, not ledger data)","request":{"method":"DELETE","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/monitors/:id","host":["{{baseUrl}}"],"path":["api","v1","monitors",":id"],"variable":[{"key":"id","value":""}]}}}]},{"name":"Notification Channels","item":[{"name":"List your notification channels","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/notification-channels","host":["{{baseUrl}}"],"path":["api","v1","notification-channels"]}}},{"name":"Create a Slack or email notification channel","request":{"method":"POST","description":"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).","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/notification-channels","host":["{{baseUrl}}"],"path":["api","v1","notification-channels"]},"body":{"mode":"raw","raw":"{\n  \"type\": \"slack\",\n  \"target\": \"string\",\n  \"name\": \"string\"\n}","options":{"raw":{"language":"json"}}}}},{"name":"Fetch one channel","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/notification-channels/:id","host":["{{baseUrl}}"],"path":["api","v1","notification-channels",":id"],"variable":[{"key":"id","value":""}]}}},{"name":"Delete a channel (hard delete; it is config, not ledger data)","request":{"method":"DELETE","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/notification-channels/:id","host":["{{baseUrl}}"],"path":["api","v1","notification-channels",":id"],"variable":[{"key":"id","value":""}]}}}]},{"name":"Policies","item":[{"name":"List your threshold policies","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/policies","host":["{{baseUrl}}"],"path":["api","v1","policies"]}}},{"name":"Create a threshold policy","request":{"method":"POST","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/policies","host":["{{baseUrl}}"],"path":["api","v1","policies"]},"body":{"mode":"raw","raw":"{\n  \"name\": \"string\",\n  \"userId\": \"string\",\n  \"appliesToAll\": true,\n  \"metric\": \"score\",\n  \"direction\": \"up\",\n  \"threshold\": 0,\n  \"component\": \"reliability\",\n  \"band\": \"string\"\n}","options":{"raw":{"language":"json"}}}}},{"name":"Fetch one policy","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/policies/:id","host":["{{baseUrl}}"],"path":["api","v1","policies",":id"],"variable":[{"key":"id","value":""}]}}},{"name":"Retune the line / rename / activate / deactivate","request":{"method":"PATCH","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/policies/:id","host":["{{baseUrl}}"],"path":["api","v1","policies",":id"],"variable":[{"key":"id","value":""}]},"body":{"mode":"raw","raw":"{\n  \"name\": \"string\",\n  \"direction\": \"up\",\n  \"threshold\": 0,\n  \"component\": \"reliability\",\n  \"band\": \"string\",\n  \"isActive\": true\n}","options":{"raw":{"language":"json"}}}}},{"name":"Delete a policy (hard delete; it is config, not ledger data)","request":{"method":"DELETE","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/policies/:id","host":["{{baseUrl}}"],"path":["api","v1","policies",":id"],"variable":[{"key":"id","value":""}]}}}]},{"name":"Ingest","item":[{"name":"Ingest YOUR payload shape via a declarative field mapping","request":{"method":"POST","description":"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).\n\nPartial 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.\n\nThe 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.\n\nMapped 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).\n\n**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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/ingest","host":["{{baseUrl}}"],"path":["api","v1","ingest"]},"body":{"mode":"raw","raw":"{\n  \"mapping\": \"string\",\n  \"mappingId\": \"string\",\n  \"records\": []\n}","options":{"raw":{"language":"json"}}}}},{"name":"List your stored mappings","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/ingest/mappings","host":["{{baseUrl}}"],"path":["api","v1","ingest","mappings"]}}},{"name":"Save a reusable named mapping","request":{"method":"POST","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/ingest/mappings","host":["{{baseUrl}}"],"path":["api","v1","ingest","mappings"]},"body":{"mode":"raw","raw":"{\n  \"name\": \"string\",\n  \"description\": \"string\",\n  \"mapping\": \"string\"\n}","options":{"raw":{"language":"json"}}}}},{"name":"Fetch one stored mapping","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/ingest/mappings/:id","host":["{{baseUrl}}"],"path":["api","v1","ingest","mappings",":id"],"variable":[{"key":"id","value":""}]}}},{"name":"Delete a stored mapping (config, not ledger data; events already ingested are untouched)","request":{"method":"DELETE","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/ingest/mappings/:id","host":["{{baseUrl}}"],"path":["api","v1","ingest","mappings",":id"],"variable":[{"key":"id","value":""}]}}},{"name":"List your CSV imports (status + counts)","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/imports","host":["{{baseUrl}}"],"path":["api","v1","imports"]}}},{"name":"Backfill historical outcomes from a CSV","request":{"method":"POST","description":"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.\n\nTwo body forms: `text/csv` with `?mappingId=` naming a stored mapping, or `application/json` with `{ csv, mapping | mappingId }`.\n\nAsync: 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.\n\nRow cap 50000 (`IMPORT_MAX_ROWS`). An over-cap file is refused with both numbers; an import is never silently truncated.\n\n**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.\n\n**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`.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/imports","host":["{{baseUrl}}"],"path":["api","v1","imports"]},"body":{"mode":"raw","raw":"{\n  \"csv\": \"string\",\n  \"mapping\": \"string\",\n  \"mappingId\": \"string\"\n}","options":{"raw":{"language":"json"}}}}},{"name":"Import status + counts","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/imports/:id","host":["{{baseUrl}}"],"path":["api","v1","imports",":id"],"variable":[{"key":"id","value":""}]}}},{"name":"Per-row failures (and warnings) so you can fix and re-upload","request":{"method":"GET","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/imports/:id/errors","host":["{{baseUrl}}"],"path":["api","v1","imports",":id","errors"],"variable":[{"key":"id","value":""}]}}}]},{"name":"Confirmations","item":[{"name":"List your confirmation requests","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/confirmations","host":["{{baseUrl}}"],"path":["api","v1","confirmations"]}}},{"name":"Propose an outcome for a counterparty to confirm","request":{"method":"POST","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/confirmations","host":["{{baseUrl}}"],"path":["api","v1","confirmations"]},"body":{"mode":"raw","raw":"{\n  \"userId\": \"string\",\n  \"eventType\": \"TRANSACTION_COMPLETED\",\n  \"stakeLevel\": \"HIGH\",\n  \"transactionValue\": 0,\n  \"dueDate\": \"string\",\n  \"completedAt\": \"string\",\n  \"metadata\": {},\n  \"counterpartyRef\": \"string\",\n  \"counterpartyName\": \"string\",\n  \"description\": \"string\",\n  \"returnUrl\": \"string\",\n  \"expiresInDays\": 0\n}","options":{"raw":{"language":"json"}}}}},{"name":"Bulk-create confirmation requests (the activation engine)","request":{"method":"POST","description":"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).","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/confirmations/batch","host":["{{baseUrl}}"],"path":["api","v1","confirmations","batch"]},"body":{"mode":"raw","raw":"{\n  \"requests\": []\n}","options":{"raw":{"language":"json"}}}}},{"name":"Fetch one of your confirmation requests","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/confirmations/:id","host":["{{baseUrl}}"],"path":["api","v1","confirmations",":id"],"variable":[{"key":"id","value":""}]}}},{"name":"Cancel a still-pending request","request":{"method":"POST","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/confirmations/:id/cancel","host":["{{baseUrl}}"],"path":["api","v1","confirmations",":id","cancel"],"variable":[{"key":"id","value":""}]}}},{"name":"What the counterparty is being asked to confirm (public, token-gated)","request":{"method":"GET","description":"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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/confirmations/:id/preview","host":["{{baseUrl}}"],"path":["api","v1","confirmations",":id","preview"],"variable":[{"key":"id","value":""}]}}},{"name":"Counterparty confirms or declines (public, token-gated)","request":{"method":"POST","description":"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.","header":[{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/confirmations/:id/respond","host":["{{baseUrl}}"],"path":["api","v1","confirmations",":id","respond"],"variable":[{"key":"id","value":""}]},"body":{"mode":"raw","raw":"{\n  \"token\": \"string\",\n  \"decision\": \"confirm\"\n}","options":{"raw":{"language":"json"}}}}}]},{"name":"Activation","item":[{"name":"Onboard your whole book in one call (the activation engine)","request":{"method":"POST","description":"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.\n\nINVARIANT: 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.\n\nPartial 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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/activation/campaigns","host":["{{baseUrl}}"],"path":["api","v1","activation","campaigns"]},"body":{"mode":"raw","raw":"{\n  \"name\": \"string\",\n  \"rows\": []\n}","options":{"raw":{"language":"json"}}}}},{"name":"Campaign funnel (submitted → confirmed / declined / pending)","request":{"method":"GET","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/activation/campaigns/:id","host":["{{baseUrl}}"],"path":["api","v1","activation","campaigns",":id"],"variable":[{"key":"id","value":""}]}}}]},{"name":"References","item":[{"name":"List your reference requests","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/references","host":["{{baseUrl}}"],"path":["api","v1","references"]}}},{"name":"Propose a résumé claim for a reference to confirm","request":{"method":"POST","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/references","host":["{{baseUrl}}"],"path":["api","v1","references"]},"body":{"mode":"raw","raw":"{\n  \"userId\": \"string\",\n  \"category\": \"education\",\n  \"label\": \"string\",\n  \"issuer\": \"string\",\n  \"jurisdiction\": \"string\",\n  \"reference\": \"string\",\n  \"counterpartyRef\": \"string\",\n  \"counterpartyName\": \"string\",\n  \"description\": \"string\",\n  \"returnUrl\": \"string\",\n  \"expiresInDays\": 0\n}","options":{"raw":{"language":"json"}}}}},{"name":"Fetch one of your reference requests","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/references/:id","host":["{{baseUrl}}"],"path":["api","v1","references",":id"],"variable":[{"key":"id","value":""}]}}},{"name":"Cancel a still-pending request","request":{"method":"POST","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/references/:id/cancel","host":["{{baseUrl}}"],"path":["api","v1","references",":id","cancel"],"variable":[{"key":"id","value":""}]}}},{"name":"What the counterparty is being asked to confirm (public, token-gated)","request":{"method":"GET","description":"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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/references/:id/preview","host":["{{baseUrl}}"],"path":["api","v1","references",":id","preview"],"variable":[{"key":"id","value":""}]}}},{"name":"Counterparty confirms or declines (public, token-gated)","request":{"method":"POST","description":"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.","header":[{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/references/:id/respond","host":["{{baseUrl}}"],"path":["api","v1","references",":id","respond"],"variable":[{"key":"id","value":""}]},"body":{"mode":"raw","raw":"{\n  \"token\": \"string\",\n  \"decision\": \"confirm\"\n}","options":{"raw":{"language":"json"}}}}}]},{"name":"Screenings","item":[{"name":"List your screening jobs (status + summary only)","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/screenings","host":["{{baseUrl}}"],"path":["api","v1","screenings"]}}},{"name":"Submit an async bulk screening (up to 10,000 ids)","request":{"method":"POST","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/screenings","host":["{{baseUrl}}"],"path":["api","v1","screenings"]},"body":{"mode":"raw","raw":"{\n  \"userIds\": []\n}","options":{"raw":{"language":"json"}}}}},{"name":"Screening status + summary (no results payload)","request":{"method":"GET","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/screenings/:id","host":["{{baseUrl}}"],"path":["api","v1","screenings",":id"],"variable":[{"key":"id","value":""}]}}},{"name":"Full per-user results (JSON, or ?format=csv attachment)","request":{"method":"GET","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/screenings/:id/results","host":["{{baseUrl}}"],"path":["api","v1","screenings",":id","results"],"variable":[{"key":"id","value":""}]}}}]},{"name":"Agents","item":[{"name":"Delivery receipts + a signed delivery credential","request":{"method":"GET","description":"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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/verify/:token/delivery-receipts","host":["{{baseUrl}}"],"path":["api","v1","verify",":token","delivery-receipts"],"variable":[{"key":"token","value":""}]}}},{"name":"Register (or update) an agent subject","request":{"method":"POST","description":"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).","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/agents","host":["{{baseUrl}}"],"path":["api","v1","agents"]},"body":{"mode":"raw","raw":"{\n  \"userId\": \"string\",\n  \"operatedByReportingPlatform\": true,\n  \"operator\": {},\n  \"modelFamily\": \"string\",\n  \"description\": \"string\"\n}","options":{"raw":{"language":"json"}}}}},{"name":"Inspect an agent subject","request":{"method":"GET","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/agents/:id","host":["{{baseUrl}}"],"path":["api","v1","agents",":id"],"variable":[{"key":"id","value":""}]}}}]},{"name":"Businesses","item":[{"name":"Register (or update) a business subject","request":{"method":"POST","description":"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.\n\nDeclaring 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.\n\nAt 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).\n\nSCOPE: 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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/businesses","host":["{{baseUrl}}"],"path":["api","v1","businesses"]},"body":{"mode":"raw","raw":"{\n  \"userId\": \"string\",\n  \"controlledByReportingPlatform\": true,\n  \"business\": {},\n  \"description\": \"string\"\n}","options":{"raw":{"language":"json"}}}}},{"name":"Inspect a business subject","request":{"method":"GET","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/businesses/:id","host":["{{baseUrl}}"],"path":["api","v1","businesses",":id"],"variable":[{"key":"id","value":""}]}}},{"name":"The counterparty-confirmed B2B record on its own","request":{"method":"GET","description":"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).","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/businesses/:id/record","host":["{{baseUrl}}"],"path":["api","v1","businesses",":id","record"],"variable":[{"key":"id","value":""}]}}},{"name":"Business record + a signed business credential","request":{"method":"GET","description":"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.\n\nThis 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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/verify/:token/business-record","host":["{{baseUrl}}"],"path":["api","v1","verify",":token","business-record"],"variable":[{"key":"token","value":""}]}}}]},{"name":"Plans","item":[{"name":"Developer plan catalog","request":{"method":"GET","description":"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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/plans","host":["{{baseUrl}}"],"path":["api","v1","plans"]}}}]},{"name":"Reference","item":[{"name":"Machine-readable error catalog","request":{"method":"GET","description":"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.\n\nThis 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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/errors","host":["{{baseUrl}}"],"path":["api","v1","errors"]}}},{"name":"Self-describing enums","request":{"method":"GET","description":"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.\n\nDerived 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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/enums","host":["{{baseUrl}}"],"path":["api","v1","enums"]}}},{"name":"Industry outcome templates: map real-world work to events","request":{"method":"GET","description":"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.\n\nEach 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**.\n\n`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.\n\nGuidance 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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/outcome-templates","host":["{{baseUrl}}"],"path":["api","v1","outcome-templates"]}}},{"name":"API version contract, changelog and deprecation policy","request":{"method":"GET","description":"The version contract for `v1` as data, plus every dated change to this surface. Public and unauthenticated.\n\n`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.\n\n`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.\n\n`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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/changelog","host":["{{baseUrl}}"],"path":["api","v1","changelog"]}}},{"name":"Adverse-action reason-code catalog","request":{"method":"GET","description":"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.\n\nBuilt 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).\n\nEvery 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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/reason-codes","host":["{{baseUrl}}"],"path":["api","v1","reason-codes"]}}}]},{"name":"Widget","item":[{"name":"Embeddable trust-badge script","request":{"method":"GET","description":"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.","header":[],"url":{"raw":"{{baseUrl}}/widget/credda-widget.js","host":["{{baseUrl}}"],"path":["widget","credda-widget.js"]}}},{"name":"Embeddable \"Confirm with Credda\" launcher","request":{"method":"GET","description":"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.\n\nCompletion 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.\n\nServed from this origin on purpose, never a `cdn.*` subdomain.","header":[],"url":{"raw":"{{baseUrl}}/verify-embed.js","host":["{{baseUrl}}"],"path":["verify-embed.js"]}}},{"name":"Completion bridge for the embedded confirmation flow","request":{"method":"GET","description":"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.","header":[],"url":{"raw":"{{baseUrl}}/verify-embed-bridge.js","host":["{{baseUrl}}"],"path":["verify-embed-bridge.js"]}}}]},{"name":"Well-Known","item":[{"name":"Verifying keys (Ed25519/OKP)","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/.well-known/jwks.json","host":["{{baseUrl}}"],"path":[".well-known","jwks.json"]}}},{"name":"did:web issuer DID document","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/.well-known/did.json","host":["{{baseUrl}}"],"path":[".well-known","did.json"]}}},{"name":"Recognized issuers","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/.well-known/credda-trust-registry.json","host":["{{baseUrl}}"],"path":[".well-known","credda-trust-registry.json"]}}},{"name":"Web Bot Auth signature directory (Ed25519 JWKS)","request":{"method":"GET","description":"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.\n\n**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.\n\nServed 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`.\n\nWeb 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.","header":[],"url":{"raw":"{{baseUrl}}/.well-known/http-message-signatures-directory","host":["{{baseUrl}}"],"path":[".well-known","http-message-signatures-directory"]}}}]},{"name":"Test Mode","item":[{"name":"Seed the sandbox with synthetic subjects (test keys only)","request":{"method":"POST","description":"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).\n\nEvery 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.\n\n**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.\n\nEach 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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/test/seed","host":["{{baseUrl}}"],"path":["api","v1","test","seed"]}}},{"name":"Reset the platform's test data (test keys only)","request":{"method":"DELETE","description":"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.","header":[{"key":"Authorization","value":"Bearer {{apiKey}}"}],"url":{"raw":"{{baseUrl}}/api/v1/test/data","host":["{{baseUrl}}"],"path":["api","v1","test","data"]}}}]},{"name":"Open Badges","item":[{"name":"The closed set of Open Badges 3.0 achievements this issuer will sign","request":{"method":"GET","description":"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.","header":[],"url":{"raw":"{{baseUrl}}/api/v1/open-badges/achievements","host":["{{baseUrl}}"],"path":["api","v1","open-badges","achievements"]}}},{"name":"One achievement definition","request":{"method":"GET","header":[],"url":{"raw":"{{baseUrl}}/api/v1/open-badges/achievements/:badgeId","host":["{{baseUrl}}"],"path":["api","v1","open-badges","achievements",":badgeId"],"variable":[{"key":"badgeId","value":""}]}}},{"name":"Sign Open Badges 3.0 credentials for an anchored subject","request":{"method":"POST","description":"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.","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/open-badges","host":["{{baseUrl}}"],"path":["api","v1","admin","open-badges"]},"body":{"mode":"raw","raw":"{\n  \"shareToken\": \"string\",\n  \"awards\": []\n}","options":{"raw":{"language":"json"}}}}}]},{"name":"Admin","item":[{"name":"List platforms (+ optional consolidated usage)","request":{"method":"GET","description":"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.","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms"]}}},{"name":"Create a platform","request":{"method":"POST","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms"]},"body":{"mode":"raw","raw":"{\n  \"name\": \"string\",\n  \"trustTier\": \"ENTERPRISE\",\n  \"referredByPlatformId\": \"string\"\n}","options":{"raw":{"language":"json"}}}}},{"name":"List a platform's API keys (metadata only)","request":{"method":"GET","description":"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.","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/keys","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","keys"],"variable":[{"key":"id","value":""}]}}},{"name":"Generate an API key (shown once)","request":{"method":"POST","description":"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.","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/keys","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","keys"],"variable":[{"key":"id","value":""}]},"body":{"mode":"raw","raw":"{\n  \"label\": \"string\",\n  \"scopes\": [],\n  \"expiresInDays\": 0,\n  \"isTest\": true\n}","options":{"raw":{"language":"json"}}}}},{"name":"Re-scope an already-issued API key in place","request":{"method":"PATCH","description":"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.","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/keys/:keyId","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","keys",":keyId"],"variable":[{"key":"id","value":""},{"key":"keyId","value":""}]},"body":{"mode":"raw","raw":"{\n  \"scopes\": []\n}","options":{"raw":{"language":"json"}}}}},{"name":"Rotate an API key with a grace overlap (new key shown once)","request":{"method":"POST","description":"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.","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/keys/:keyId/rotate","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","keys",":keyId","rotate"],"variable":[{"key":"id","value":""},{"key":"keyId","value":""}]},"body":{"mode":"raw","raw":"{\n  \"graceHours\": 0\n}","options":{"raw":{"language":"json"}}}}},{"name":"Update trust tier / deactivate / velocity-exempt","request":{"method":"PATCH","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id"],"variable":[{"key":"id","value":""}]},"body":{"mode":"raw","raw":"{\n  \"trustTier\": \"ENTERPRISE\",\n  \"isActive\": true,\n  \"velocityExempt\": true\n}","options":{"raw":{"language":"json"}}}}},{"name":"Clear a velocity flag and recompute","request":{"method":"POST","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/users/:id/resolve-velocity-flag","host":["{{baseUrl}}"],"path":["api","v1","admin","users",":id","resolve-velocity-flag"],"variable":[{"key":"id","value":""}]}}},{"name":"Paginated audit trail","request":{"method":"GET","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/users/:id/audit-log","host":["{{baseUrl}}"],"path":["api","v1","admin","users",":id","audit-log"],"variable":[{"key":"id","value":""}]}}},{"name":"Withdraw a subject's public trust surface (admin mirror)","request":{"method":"DELETE","description":"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.","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/users/:id/share-token","host":["{{baseUrl}}"],"path":["api","v1","admin","users",":id","share-token"],"variable":[{"key":"id","value":""}]}}},{"name":"List a platform's webhooks","request":{"method":"GET","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/webhooks","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","webhooks"],"variable":[{"key":"id","value":""}]}}},{"name":"Subscribe an HTTPS endpoint on behalf of a platform (secret returned once)","request":{"method":"POST","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/webhooks","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","webhooks"],"variable":[{"key":"id","value":""}]},"body":{"mode":"raw","raw":"{\n  \"url\": \"string\",\n  \"events\": [],\n  \"description\": \"string\"\n}","options":{"raw":{"language":"json"}}}}},{"name":"Update url/events/description/isActive","request":{"method":"PATCH","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/webhooks/:webhookId","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","webhooks",":webhookId"],"variable":[{"key":"id","value":""},{"key":"webhookId","value":""}]}}},{"name":"Delete a webhook","request":{"method":"DELETE","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/webhooks/:webhookId","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","webhooks",":webhookId"],"variable":[{"key":"id","value":""},{"key":"webhookId","value":""}]}}},{"name":"Send a synthetic signed delivery","request":{"method":"POST","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/webhooks/:webhookId/test","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","webhooks",":webhookId","test"],"variable":[{"key":"id","value":""},{"key":"webhookId","value":""}]}}},{"name":"Recent delivery attempts","request":{"method":"GET","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/webhooks/:webhookId/deliveries","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","webhooks",":webhookId","deliveries"],"variable":[{"key":"id","value":""},{"key":"webhookId","value":""}]}}},{"name":"Replay a past delivery (admin mirror)","request":{"method":"POST","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/webhooks/:webhookId/deliveries/:deliveryId/replay","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","webhooks",":webhookId","deliveries",":deliveryId","replay"],"variable":[{"key":"id","value":""},{"key":"webhookId","value":""},{"key":"deliveryId","value":""}]}}},{"name":"List a platform's live score monitors","request":{"method":"GET","description":"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.","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/monitors","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","monitors"],"variable":[{"key":"id","value":""}]}}},{"name":"Create a score monitor on behalf of a platform","request":{"method":"POST","description":"Admin mirror of POST /api/v1/monitors: identical schema, conditions and per-tier active-monitor cap (409 MONITOR_LIMIT_REACHED, `details` carries `{limit, tier}`).","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/monitors","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","monitors"],"variable":[{"key":"id","value":""}]},"body":{"mode":"raw","raw":"{\n  \"userId\": \"string\",\n  \"belowScore\": 0,\n  \"aboveScore\": 0,\n  \"onBandChange\": true\n}","options":{"raw":{"language":"json"}}}}},{"name":"Fetch one monitor (admin mirror)","request":{"method":"GET","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/monitors/:monitorId","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","monitors",":monitorId"],"variable":[{"key":"id","value":""},{"key":"monitorId","value":""}]}}},{"name":"Update thresholds / onBandChange / isActive (admin mirror)","request":{"method":"PATCH","description":"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.","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"},{"key":"Content-Type","value":"application/json"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/monitors/:monitorId","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","monitors",":monitorId"],"variable":[{"key":"id","value":""},{"key":"monitorId","value":""}]},"body":{"mode":"raw","raw":"{\n  \"belowScore\": 0,\n  \"aboveScore\": 0,\n  \"onBandChange\": true,\n  \"isActive\": true\n}","options":{"raw":{"language":"json"}}}}},{"name":"Delete a monitor (admin mirror)","request":{"method":"DELETE","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/monitors/:monitorId","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","monitors",":monitorId"],"variable":[{"key":"id","value":""},{"key":"monitorId","value":""}]}}},{"name":"A platform's usage vs. tier quota, on behalf of it","request":{"method":"GET","description":"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.","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/usage","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","usage"],"variable":[{"key":"id","value":""}]}}},{"name":"A platform's own activity log, on behalf of it","request":{"method":"GET","description":"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.","header":[{"key":"X-Admin-Secret","value":"{{adminSecret}}"}],"url":{"raw":"{{baseUrl}}/api/v1/admin/platforms/:id/activity","host":["{{baseUrl}}"],"path":["api","v1","admin","platforms",":id","activity"],"variable":[{"key":"id","value":""}]}}}]}]}