{"openapi":"3.1.0","info":{"title":"Credda Reliability Score API","version":"1.0","description":"Portable trust infrastructure. Immutable event ingestion → deterministic scoring → offline-verifiable credentials (Credda + W3C) → outbound webhooks. The score is a pure function of an append-only event ledger; no human or AI adjusts it.\n\n**Bias-free by construction.** Disputes resolve **deterministically** from the tamper-proof commitment record; no reviewer discretion decides an outcome. AI is **advisory only** (score coaching, plain-language explanations, evidence summaries, and reading the signals that feed a score) and never sets, changes, or decides a score. AI can help *gather and interpret* the evidence that becomes ledger events, but only recorded events, never an AI verdict, move the score.\n\n## Conventions\n- **Auth & scopes:** platform key via `Authorization: Bearer crd_live_…`. Keys carry method-based scopes: `read` (GET/HEAD), `write` (mutations), or `*` (both).\n- **Rate limits:** per-minute, tier-scaled; standard `RateLimit-*` headers. Monthly volume **quotas** may also apply (`X-Quota-*` headers). Both return `429`.\n- **Tracing:** every response carries `X-Request-Id` (send your own and we echo it, so your logs and ours share one id); it is included in every error body. Quoting a request id is the fastest route to a diagnosis.\n- **Errors:** JSON `{ error, code, requestId, retryable }` (+ `details` where useful). The full catalogue (what each code means, what to do about it, and whether a retry can help) is served at `GET /api/v1/errors` and mirrored in `x-error-codes`.\n- **Retries:** retry ONLY codes with `retryable: true`, and honour `Retry-After` (seconds) when present. Retrying anything else fails identically and burns quota.\n- **Enums:** every closed value set (`eventType`, `stakeLevel`, `scoreBand`, `disputeStatus`, `platformTier`) is served, described, at `GET /api/v1/enums`.\n- **Idempotency:** `Idempotency-Key` on mutations makes retries exactly-once.\n- **Versioning:** `v1` is **additive-only**: new endpoints, new optional inputs and new response fields, never a removal or a change of meaning. Ignore response fields and enum values you do not recognise. The full contract, the dated changelog and the deprecation policy are served at `GET /api/v1/changelog`. A deprecated endpoint would answer with `Deprecation` (RFC 9745) and `Sunset` (RFC 8594) headers, at least 180 days ahead; nothing is deprecated today.\n\n## Test mode (sandbox)\nEvery platform can hold **test keys** (`crd_test_…`) alongside live ones (`crd_live_…`): free at every tier, same endpoints, same scopes, same rate limits, no request-shape changes: the key selects the mode. A test key writes and reads ONLY the platform's private **test universe**: test users are namespaced internally per platform, so they can never collide with (or read) real users, and a live key never sees test data. Test users run through the IDENTICAL deterministic scoring formula (that is the point of a sandbox). Hard exclusions: test data can never become portable trust: share tokens, credentials, verify pages and exports refuse test users (`TEST_MODE_NOT_ALLOWED`). Webhooks fire normally for test activity, with `\"livemode\": false` on the delivery envelope (live deliveries carry `\"livemode\": true`). Test requests are metered for observability but do **not** count toward the monthly plan quota. `DELETE /api/v1/test/data` resets the platform's test universe, the only deletion path in the product, possible only because every row it touches is test data by construction; the live ledger stays append-only.","contact":{"name":"Credda","email":"martin@credda.io","url":"https://credda.io"},"license":{"name":"Proprietary"}},"x-api-version":"v1","x-webhook-events":{"score.updated":{"description":"A user's score changed (or was first computed).","payloadSchema":"ScoreEventData"},"score.band_changed":{"description":"A user's reliability band changed.","payloadSchema":"ScoreEventData"},"dispute.resolved":{"description":"A dispute on the platform’s event was adjudicated or auto-lapsed.","payloadSchema":"DisputeResolvedData"},"monitor.triggered":{"description":"A score monitor fired: the score crossed a registered threshold (edge-triggered) or the band changed.","payloadSchema":"MonitorTriggeredData"},"policy.threshold_crossed":{"description":"A threshold policy fired: a subject crossed a registered line (score/component/verified-event crossing, or a band transition). Carries deterministic evidence + a decision_input block for the partner's OWN policy engine. Evidence, never a decision.","payloadSchema":"PolicyThresholdCrossedData"},"usage.quota_warning":{"description":"The platform's month-to-date usage crossed the warning share of its monthly quota (once per period).","payloadSchema":"UsageQuotaWarningData"},"import.completed":{"description":"An async historical import job reached its terminal state (COMPLETED with counts, or FAILED). Advisory: stop polling for status.","payloadSchema":"ImportCompletedData"},"screening.completed":{"description":"An async bulk screening job reached its terminal state (COMPLETED with foundCount, or FAILED). Advisory: stop polling for status.","payloadSchema":"ScreeningCompletedData"},"confirmation.awaiting_response":{"description":"A confirmation request you created is still PENDING past the reminder threshold (default 48h). Re-send the link over your own channel; Credda delivers nothing to anyone and never contacts a counterparty. Once per request; the raw token is never included.","payloadSchema":"ConfirmationReminderData"},"confirmation.expiring_soon":{"description":"A PENDING confirmation request is approaching its expiry (default within 24h), the last window for the counterparty to act. Once per request, independent of confirmation.awaiting_response.","payloadSchema":"ConfirmationReminderData"}},"x-error-codes":{"ACTIVATION_NO_VALID_ROWS":"POST /activation/campaigns: every submitted row either failed its own check (e.g. counterpartyRef equals the subject → CONFIRMATION_SELF) or was an in-batch duplicate of an earlier row. The campaign is not persisted; an empty campaign is never created. The `details` object carries the per-row `results` and dropped `duplicates`. (400)","ACTIVITY_ACTION_INVALID":"GET /activity: `action` is not one of the platform-visible audit actions. (400)","ACTIVITY_FORMAT_INVALID":"GET /activity: `format` must be `json` (default) or `csv`. CSV is the SIEM/audit-export shape; paginate it with the `X-Next-Cursor` response header. (400)","ACTIVITY_RANGE_INVALID":"GET /activity: `from`/`to` is not a valid ISO timestamp or date, or `from` is after `to`. (400)","ANALYTICS_RANGE_INVALID":"GET /analytics/events and /analytics/scores: `from`/`to` is missing its pair, is not a valid ISO date (YYYY-MM-DD), or `from` is after `to`. (400)","ANALYTICS_WINDOW_CONFLICT":"GET /analytics/events and /analytics/scores: `days` and `from`/`to` are mutually exclusive. (400)","BAD_REQUEST":"The request was malformed or failed a check that has no more specific code. (400)","BOOK_FORMAT_INVALID":"GET /users: `format` must be `json` (default) or `csv`. (400)","BOOK_QUERY_INVALID":"GET /users: a filter, sort, order, limit or cursor parameter is out of range or not one of the allowed values. The filter set is closed; there is no free-form query. (400)","BUSINESS_IDENTITY_NOT_DECLARED":"POST /businesses: a business subject must declare at least one identity. Without a name or domain, an outcome that names the business as its own counterparty cannot be detected as self-dealing. (400)","CAREER_FORMAT_INVALID":"GET /users/{id}/career-export and GET /verify/{token}/career-export: `format` must be `jsonresume` (JSON Resume, the default) or `jsonld` (a schema.org Person graph). (400)","CONFIRMATION_NOT_PENDING":"The confirmation request has already been confirmed, declined, cancelled, or has expired, so it cannot be actioned again. The token is single-use. (409)","CONFIRMATION_SELF":"POST /confirmations: `counterpartyRef` names the subject itself (case/whitespace-insensitive). A subject can never be its own independent witness. (400)","CONFIRMATION_TOKEN_INVALID":"The confirmation preview/respond endpoint requires the one-time `token` returned when the request was created, and it did not match. (401)","CONFLICT":"The resource is not in a state where this operation is valid, e.g. a dispute that is already resolved. (409)","DISPATCH_CONTEXT_INVALID":"GET /users/{id}/reliability: the `context` query parameter is not one this API serves. (400)","EXPORT_FORMAT_INVALID":"GET /events/export: `format` must be `json` (default) or `csv`. (400)","EXPORT_RANGE_INVALID":"GET /events/export: `from`/`to` is not a valid ISO timestamp or date, or `from` is after `to`. (400)","FORBIDDEN":"The key is valid but is not allowed to perform this operation on this resource. (403)","IDEMPOTENCY_KEY_REUSED":"The same `Idempotency-Key` was already used for a request whose body differed. Replaying it would be ambiguous, so it is refused rather than guessed at. (422)","IMPORT_CSV_INVALID":"POST /imports: an unbalanced quote, a blank or duplicate header, or a file with a header but no data rows. (400)","IMPORT_QUEUE_UNAVAILABLE":"POST /imports: the async queue is unreachable and the file is too large to run inline, so it was refused rather than accepted and silently dropped. (503); retryable; honour Retry-After.","IMPORT_TOO_MANY_ROWS":"POST /imports: the file is larger than IMPORT_MAX_ROWS. Refused outright; an import is never silently truncated. (400)","INGEST_MAPPING_INVALID":"POST /ingest, /ingest/mappings or /imports: the mapping names an unknown Credda field or an unknown transform, or a rule reads nothing. (400)","INTERNAL_ERROR":"Something failed on our side. The failure is logged against the `requestId` in the response. (500); retryable; honour Retry-After.","MONITOR_LIMIT_REACHED":"Your plan's cap on simultaneously active score monitors is reached. `details` carries `{limit, tier}`. (409)","NOTIFICATION_CHANNEL_INVALID":"POST /notification-channels: `type` must be `slack` or `email`, and `target` must match it: a Slack target must be an https `hooks.slack.com` incoming-webhook URL, an email target a valid address. The message names the specific problem. (400)","NOTIFICATION_CHANNEL_LIMIT_REACHED":"The per-platform cap on notification channels is reached (`details` carries `{limit}`). Channels are budgeted separately per mode (test vs live). (409)","NOT_FOUND":"No resource matched the id or path. For users, this also means no events have been reported yet; see USER_NOT_FOUND, which additionally covers the brief asynchronous window after a first write. (404)","OPERATOR_NOT_DECLARED":"POST /agents: registering an agent you do not operate requires naming the operator, otherwise it cannot be checked for self-dealing. (400)","PAYLOAD_NOT_RETAINED":"This webhook delivery predates payload retention, so there is no stored body to replay. (409)","PLAN_REQUIRED":"The key authenticated and carries the right scope, but this capability is reserved for a higher tier (e.g. async bulk screening or agent subjects are Enterprise). `details` carries `{feature, currentTier, requiredPlan}`. Sandbox (test-mode) keys are never gated; try the capability with a `crd_test_` key first. (402)","POLICY_LIMIT_REACHED":"Your plan's cap on simultaneously active threshold policies is reached. `details` carries `{limit, tier, mode}`. Policies are budgeted separately from monitors. (409)","QUOTA_EXCEEDED":"This platform's monthly request quota is spent. Test-mode requests never count toward it. (429); retryable; honour Retry-After.","RATE_LIMIT_EXCEEDED":"Too many requests in the current minute window for this platform (or IP, for unauthenticated traffic). Limits are tier-scaled; see GET /api/v1/plans. (429); retryable; honour Retry-After.","REFERENCE_NOT_PENDING":"The reference request has already been confirmed, declined, cancelled, or has expired, so it cannot be actioned again. The token is single-use. (409)","REFERENCE_SELF":"POST /references: `counterpartyRef` names the subject itself (case/whitespace-insensitive). A person can never be their own reference. (400)","REFERENCE_TOKEN_INVALID":"The reference preview/respond endpoint requires the one-time `token` returned when the request was created, and it did not match. (401)","RESERVED_EXTERNAL_ID":"Live ingestion refuses ids beginning with `test:`: that prefix is reserved for the per-platform sandbox namespace. (400)","SCOPE_CEILING_EXCEEDED":"POST /platform/keys: a key can only mint another key scoped AT OR BELOW its own capabilities; otherwise a narrow key could bootstrap itself to full access. `details.exceeded` lists the `<resource>:<action>` capabilities requested that the calling key does not itself hold. (403)","SCOPE_INSUFFICIENT":"The key authenticated, but does not carry the `<resource>:<action>` scope this route requires (e.g. `events:write`). Scopes are set per key and default to your plan's. (403)","SCREENING_NOT_COMPLETED":"Results were requested for a job that is still QUEUED/RUNNING, or that FAILED. (409)","SCREENING_QUEUE_UNAVAILABLE":"The async queue is unreachable and the job is too large to run inline, so it was refused rather than accepted and silently lost. (503); retryable; honour Retry-After.","STREAM_LIMIT_REACHED":"GET /api/v1/events/stream holds one long-lived connection (and one dedicated Redis subscriber) per open stream, so the number a platform may hold at once is capped. `details` carries `{limit}`. (429); retryable; honour Retry-After.","TEST_MODE_NOT_ALLOWED":"Share tokens, credentials, verify pages and trust exports refuse test users; test data can never become portable trust. (403)","TEST_MODE_ONLY":"DELETE /test/data only accepts a `crd_test_` key. Live data is append-only and cannot be deleted. (403)","UNAUTHORIZED":"No `Authorization: Bearer` header, or the key is unknown, revoked, inactive or expired. (401)","USAGE_FORMAT_INVALID":"GET /usage: `format` must be `json` (default) or `csv`. (400)","USAGE_RANGE_INVALID":"GET /usage: `from`/`to` is missing its pair, is not a valid ISO date, or `from` is after `to`. (400)","USAGE_WINDOW_CONFLICT":"GET /usage: `days` and `from`/`to` are mutually exclusive. (400)","USER_NOT_FOUND":"No events have been reported for this externalId, so the user does not exist yet in your ledger. This is also what you see in the short window after a first write: scoring is recomputed ASYNCHRONOUSLY, so a read taken immediately after the event that created the subject can still miss it. (404)","VALIDATION_ERROR":"The JSON body did not match the endpoint schema. The response carries a `details` array with one entry per problem: `{path, message, code, expected?, received?}`. (400)"},"servers":[{"url":"https://api.credda.io","description":"Production"}],"tags":[{"name":"Events","description":"Append-only outcome ingestion (the only score input)."},{"name":"Scores","description":"Read the deterministic score and its explanation."},{"name":"Analytics","description":"Tenant-scoped time-series aggregates over the caller's OWN data: event volume by day and type with the verified share, and the caller's subjects' score band mix, central tendency and movement over a window. Aggregate-only, read-only (scores:read), test/live isolated; nothing here computes or writes a score, and the numbers are distribution facts, never a verdict on any subject."},{"name":"Webhooks","description":"Subscribe to signed trust events (advisory, never affects a score)."},{"name":"Monitors","description":"Continuous score monitoring: edge-triggered threshold/band watches that fire monitor.triggered webhook events. Notification config only; never affects a score."},{"name":"Notification Channels","description":"Slack + email destinations that receive the SAME trust events as webhooks, for a partner who runs no webhook receiver. Delivery is fire-and-forget; notification config only, never affects a score."},{"name":"Policies","description":"Threshold policies: declarative, partner-grade rules that fire a policy.threshold_crossed webhook when a subject (one user, or all your subjects) crosses a score/component/verified-event line or a band. Credda supplies evidence + a decision_input block; the partner owns the decision and the loss. Notification config only; never affects a score."},{"name":"Confirmations","description":"Counterparty confirmation requests: propose an outcome and have a DISTINCT party confirm it through a one-time token, so isVerified is EARNED by a third-party witness rather than asserted by the reporting platform. A pending request writes no event and moves no score; only a genuine confirmation writes to the ledger (through the same append-only path as POST /events). The confirm/preview endpoints are token-gated and unauthenticated: a counterparty holds a token, not an API key."},{"name":"Activation","description":"Activation campaigns: the confirmation primitive AT SCALE. Submit your whole historical book (roster / timesheets) in one call; each row becomes an UNCONFIRMED confirmation request fanned out to its named counterparty, and the campaign reports the funnel (submitted → confirmed / declined / pending) as those tokens are acted on. A campaign writes NO events: every row flows through the same create → confirm path a single confirmation uses, so isVerified is earned only on a genuine counterparty confirm, never here. NO VERDICT: a campaign counts asks and confirmations, it never rates the subject."},{"name":"References","description":"Reference / employment-verification requests: the qualifications-half sibling of confirmations. Propose a résumé claim (employment/education/certification/skill) and have the named third party who was there (a past employer, manager, client, colleague, or issuing body) confirm it through a one-time token, turning a self-attested qualification VERIFIED. A pending/declined/expired/cancelled request records nothing; only a genuine confirmation records the qualification (verified, decided by the one witness valve, never hardcoded) and a qualification NEVER moves the reliability score. The confirm/preview endpoints are token-gated and unauthenticated. NO VERDICT: a reference attests a fact (this person held this role / earned this credential), never a rating or recommendation."},{"name":"Screenings","description":"Async bulk screening jobs: batch score reads at roster scale (thousands of ids). Strictly read-only: a screening never writes events, snapshots, or anything score-side."},{"name":"Ingest","description":"Data ingress without client-side transformation: POST your OWN payload shape alongside a declarative field mapping, or backfill history from a CSV. Both write through the SAME append-only path as POST /events (idempotency, velocity guard, audit trail, score recomputation); neither contains any scoring logic."},{"name":"Agents","description":"Non-human subjects and their counterparty-confirmed delivery receipts. Same ledger, same deterministic formula, one extra integrity rule: an agent's own operator can never confirm its work. A delivery record, not a safety, alignment or capability rating, and never a recommendation."},{"name":"Businesses","description":"Businesses as first-class scored subjects. Same ledger, same deterministic formula, one extra integrity rule: a business can never confirm its own outcomes, not by reporting them itself, and not by naming itself as its own counterparty. A record of outcomes OTHER businesses confirmed. Business-to-business only: Credda holds no customer/patient/consumer register, and a business's record is never derived from the people who work there."},{"name":"Disputes","description":"Resolve a dispute on one of your events."},{"name":"Earnings","description":"Verified Earnings: a portable attestation of income already recorded on the ledger, plus a signed, offline-verifiable earnings credential. Pure derivation: no scoring logic, and nothing here can move a score. Not an income verification for a credit decision and not a consumer report."},{"name":"Professional Record","description":"A worker-OWNED, résumé-shaped summary of a VERIFIED work record: reliability band, verified-outcome counts, verification depth and tenure, plus a signed, offline-verifiable credential and a LinkedIn \"Add to profile\" deep link. Pure derivation: no scoring logic, nothing here can move a score. It describes the record the subject chose to present; it is NOT a hiring recommendation, a background check, or a consumer report."},{"name":"Verified Profile","description":"The QUALIFICATIONS half of the professional record: education, skills, certifications, employment. A deterministic, BIAS-FREE measure of how much of the claimed record is independently third-party verified. It counts WHETHER a claim is verified, never how prestigious it is: no school, employer, degree or credential is ranked or weighted. Complements the Professional Record tag (which summarises the RELIABILITY record). A SEPARATE measure from the Reliability Score that can never move it, and verification of claims, not an assessment of the person."},{"name":"Verify","description":"Public, PII-free verification + Verifiable Credentials."},{"name":"Widget","description":"Dependency-free browser embeds, served from this origin (never a CDN subdomain): the display-only trust badge, and the \"Confirm with Credda\" launcher that opens the hosted confirmation flow in a sandboxed modal and reports a status back to the host page."},{"name":"Well-Known","description":"Public keys, DID document, trust registry, Web Bot Auth signature directory."},{"name":"Wallets","description":"Wallet-compatible issuance: OpenID for Verifiable Credential Issuance (openid-4-verifiable-credential-issuance-1_0 (final, 2025-09-16)) with SD-JWT VC (draft-ietf-oauth-sd-jwt-vc-17), so a holder can keep their Credda credential in any OID4VCI wallet instead of coming back to Credda for it. Selective disclosure moves the \"how much do I reveal\" choice from issuance to presentation. Issuance is implemented; the OID4VP request side is not (see docs/WALLETS.md)."},{"name":"Open Badges","description":"Open Badges 3.0 (1EdTech, document version 1.4.5): the closed, publicly readable set of achievements this issuer signs, plus the operator-only endpoint that attaches the proof. A badge records an outcome already on the ledger; it is never a rating or a recommendation, and nothing here can move a score."},{"name":"Admin","description":"Operator-only provisioning (X-Admin-Secret)."},{"name":"Platform","description":"Programmatic self-administration: manage your OWN platform (identity, API keys, scopes) entirely via the API, no dashboard. Platform-key-authenticated and gated by the `admin` scope; every route is tenant-isolated and a minted key can never exceed the minting key. Nothing here touches a score, a plan, or a trust tier."},{"name":"System","description":"Health & readiness."},{"name":"Plans","description":"Developer plan catalog: tiers, scopes, rate limits, features."},{"name":"Benchmarks","description":"Aggregate cohort comparison: where a score falls relative to a population. Legitimate, ledger-derived cohorts only (never a protected class or demographic proxy); k-anonymity enforced (no statistic below the floor); deterministic and read-only. A distribution fact, never a rating, ranking, or verdict, and nothing here can move a score."},{"name":"Reference","description":"Machine-readable reference data: the error catalog and the wire enums, both derived from the constants the code enforces so they cannot drift."},{"name":"Activity","description":"Platform transparency: the audit trail of what your own keys and config did, plus own-data export. Read-only observability; never affects a score."},{"name":"Test Mode","description":"Sandbox utilities for crd_test_ keys. Test data lives in a per-platform namespace, never touches real trust, and is the only data in the product that can be deleted."}],"security":[{"platformKey":[]}],"paths":{"/health":{"get":{"tags":["System"],"summary":"Liveness","security":[],"responses":{"200":{"description":"OK"}}}},"/health/ready":{"get":{"tags":["System"],"summary":"Readiness (checks DB, Redis, credentials)","security":[],"responses":{"200":{"description":"Ready"},"503":{"description":"Degraded"}}}},"/status.json":{"get":{"tags":["System"],"summary":"Public status: live health + latency, plus durable uptime history","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.","security":[],"responses":{"200":{"description":"Current status","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["operational","degraded","down"]},"apiVersion":{"type":"string"},"formulaVersion":{"type":"string","example":"5.3"},"commit":{"type":"string"},"uptimeSeconds":{"type":"integer","description":"Seconds since this process booted."},"checks":{"type":"object","additionalProperties":{"type":"string"}},"requests":{"type":"object","additionalProperties":{"type":"integer"}},"latencyMs":{"type":"object","additionalProperties":true},"measuringSince":{"type":"string","format":"date-time"},"history":{"type":"object","nullable":true,"description":"Durable snapshot-based history; null when unreadable (fail-open).","properties":{"basis":{"type":"string","enum":["snapshots"]},"snapshotIntervalSeconds":{"type":"integer"},"uptime":{"type":"object","properties":{"last24h":{"$ref":"#/components/schemas/UptimeWindow"},"last7d":{"$ref":"#/components/schemas/UptimeWindow"}}},"latency":{"type":"array","description":"p95 latency per snapshot, oldest→newest, bounded (last 48 snapshots).","items":{"type":"object","properties":{"capturedAt":{"type":"string","format":"date-time"},"p95":{"type":"number","nullable":true}}}}}}}}}}}}}},"/api/v1/events":{"post":{"tags":["Events"],"summary":"Ingest an outcome event","description":"Appends an immutable event and asynchronously recomputes the score. Idempotent via `Idempotency-Key`.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Opaque, printable-ASCII string (8–255 chars). Makes the request safe to retry: the same key + payload replays the stored response (`Idempotent-Replay: true`) instead of re-executing. See /docs → Idempotency.","schema":{"type":"string","minLength":8,"maxLength":255}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventInput"}}}},"responses":{"201":{"description":"Event recorded","content":{"application/json":{"schema":{"type":"object","properties":{"event":{"type":"object","additionalProperties":true},"userId":{"type":"string"},"dispute":{"type":"object","additionalProperties":true}}}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Idempotency-Key still processing","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Idempotency-Key reused with a different payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/events/batch":{"post":{"tags":["Events"],"summary":"Ingest a batch of outcome events","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.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["events"],"properties":{"events":{"type":"array","minItems":1,"maxItems":100,"items":{"allOf":[{"$ref":"#/components/schemas/EventInput"},{"type":"object","properties":{"idempotencyKey":{"type":"string","description":"Per-item idempotency key (8–255 chars): a replay is a no-op returning the original event id."}}}]}}}}}}},"responses":{"200":{"description":"Batch processed (partial success)","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"integer"},"created":{"type":"integer"},"duplicate":{"type":"integer"},"failed":{"type":"integer"},"results":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer"},"userId":{"type":"string"},"status":{"type":"string","enum":["created","duplicate","failed"]},"eventId":{"type":"string"},"error":{"type":"string"}}}}}}}}},"400":{"description":"Validation error (0 or >100 events)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/events/export":{"get":{"tags":["Events"],"summary":"Export your own reported events (data portability)","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`).","parameters":[{"name":"from","in":"query","required":false,"description":"Inclusive lower bound on the event `createdAt` (ISO timestamp or YYYY-MM-DD).","schema":{"type":"string"}},{"name":"to","in":"query","required":false,"description":"Inclusive upper bound on the event `createdAt` (ISO timestamp or YYYY-MM-DD).","schema":{"type":"string"}},{"name":"format","in":"query","required":false,"description":"'json' (default, cursor-paginated) or 'csv' (streamed, row-capped with an explicit truncation marker).","schema":{"type":"string","enum":["json","csv"]}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"JSON page size (1–1000, default 100). Ignored for CSV.","schema":{"type":"integer","minimum":1,"maximum":1000}}],"responses":{"200":{"description":"The platform's own events (JSON page by default; CSV stream when format=csv)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ExportedEvent"}},"nextCursor":{"type":"string","nullable":true,"description":"Pass as `cursor` to fetch the next page; `null` when there is no more data."}}}},"text/csv":{"schema":{"type":"string","example":"id,userId,eventType,stakeLevel,isVerified,autoImported,transactionValue,dueDate,completedAt,daysLate,createdAt,metadata\r\nevt_1,user-42,CONTRACT_FULFILLED,MEDIUM,true,false,250,2026-07-01T00:00:00.000Z,2026-06-30T12:00:00.000Z,-1,2026-06-30T12:00:01.000Z,\r\n"}}}},"400":{"description":"Invalid query (EXPORT_FORMAT_INVALID / EXPORT_RANGE_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks events:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/events/stream":{"get":{"tags":["Webhooks"],"summary":"Real-time event stream (Server-Sent Events)","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.","parameters":[{"name":"types","in":"query","required":false,"description":"Comma-separated event types to include (default: all). An unknown type is a 400.","schema":{"type":"string"}},{"name":"lastEventId","in":"query","required":false,"description":"Resume cursor: replay buffered events after this id (best-effort). Prefer the `Last-Event-ID` header, which browsers set automatically on reconnect.","schema":{"type":"string"}},{"name":"Last-Event-ID","in":"header","required":false,"description":"Resume cursor set automatically by browser EventSource on reconnect. Same effect as `?lastEventId=`.","schema":{"type":"string"}}],"responses":{"200":{"description":"An open Server-Sent Events stream. Content-Type is text/event-stream; the body is an unbounded sequence of SSE records, one per event, plus periodic `:` heartbeat comments. Not a JSON body.","content":{"text/event-stream":{"schema":{"type":"string","description":"SSE records: `id: <eventId>\\nevent: <type>\\ndata: <envelope JSON>\\n\\n`, interleaved with `:` heartbeat comments."},"example":"retry: 5000\n\n:\n\nid: 5f3a9c21-…\nevent: score.updated\ndata: {\"id\":\"5f3a9c21-…\",\"type\":\"score.updated\",\"livemode\":true,\"createdAt\":\"2026-07-24T12:00:00.000Z\",\"data\":{\"user\":{\"externalId\":\"seller_88\"},\"score\":72,\"band\":\"Good\"}}\n\n"}}},"400":{"description":"Unknown eventType in ?types","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the webhooks scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/activity":{"get":{"tags":["Activity"],"summary":"Your own activity log (self-serve audit trail + CSV export)","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.","parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size (1–100, default 25).","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"action","in":"query","required":false,"description":"Filter to one audit action.","schema":{"type":"string","enum":["EVENT_CREATED","VELOCITY_FLAG_SET","PLATFORM_CREATED","PLATFORM_UPDATED","API_KEY_GENERATED","API_KEY_ROTATED","API_KEY_REVOKED","SHARE_TOKEN_MINTED","SHARE_TOKEN_REVOKED","CREDENTIAL_OFFER_CREATED","WEBHOOK_CREATED","WEBHOOK_UPDATED","WEBHOOK_DELETED","MONITOR_CREATED","MONITOR_UPDATED","MONITOR_DELETED","NOTIFICATION_CHANNEL_CREATED","NOTIFICATION_CHANNEL_DELETED","CONFIRMATION_REQUEST_CREATED","CONFIRMATION_REQUEST_CONFIRMED","CONFIRMATION_REQUEST_DECLINED","CONFIRMATION_REQUEST_CANCELLED","TEST_DATA_RESET","TEST_DATA_SEEDED"]}},{"name":"from","in":"query","required":false,"description":"Inclusive lower bound on `createdAt` (ISO timestamp or YYYY-MM-DD).","schema":{"type":"string"}},{"name":"to","in":"query","required":false,"description":"Inclusive upper bound on `createdAt` (ISO timestamp or YYYY-MM-DD).","schema":{"type":"string"}},{"name":"format","in":"query","required":false,"description":"Response format: `json` (default) or `csv`. CSV columns: id,createdAt,action,requestId,payload.","schema":{"type":"string","enum":["json","csv"]}}],"responses":{"200":{"description":"Activity page (JSON) or CSV export (attachment; next page in the X-Next-Cursor header)","headers":{"X-Next-Cursor":{"description":"Cursor for the next CSV page; absent on the last page.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ActivityEntry"}},"nextCursor":{"type":"string","nullable":true,"description":"Pass as `cursor` to fetch the next page; `null` when there is no more data."}}}},"text/csv":{"schema":{"type":"string","example":"id,createdAt,action,requestId,payload\r\nlog_1,2026-07-23T12:00:00.000Z,API_KEY_GENERATED,req_abc,\"{\"\"platformId\"\":\"\"plat_1\"\",\"\"label\"\":\"\"ci\"\"}\"\r\n"}}}},"400":{"description":"Invalid query (ACTIVITY_ACTION_INVALID / ACTIVITY_RANGE_INVALID / ACTIVITY_FORMAT_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks usage:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/platform":{"get":{"tags":["Platform"],"summary":"Your platform identity, tier, rate limit, and the calling key's scopes","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.","responses":{"200":{"description":"Platform self-info","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"tier":{"type":"string"},"isActive":{"type":"boolean"},"rateLimitPerMin":{"type":"integer"},"keyCount":{"type":"integer"},"createdAt":{"type":"string","format":"date-time"},"key":{"type":"object","properties":{"scopes":{"type":"array","items":{"type":"string"}},"mode":{"type":"string","enum":["live","test"]}}}}}}}},"401":{"description":"Missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the admin scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/platform/keys":{"get":{"tags":["Platform"],"summary":"List your own API keys (metadata only)","description":"Key metadata for THIS platform only: id, label, scopes, isTest, isActive, expiresAt, lastUsedAt, createdAt. Never a hash, never plaintext. Requires the `admin` scope.","responses":{"200":{"description":"API keys","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"401":{"description":"Missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the admin scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Platform"],"summary":"Mint a new API key for your platform (shown once)","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.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["label"],"properties":{"label":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"},"description":"Must be a subset of the calling key's capabilities; defaults to inheriting them."},"expiresInDays":{"type":"integer","minimum":1,"maximum":3650},"isTest":{"type":"boolean","description":"Issue a sandbox (crd_test_) key. A live key may issue either; a test key may only issue test keys."}}}}}},"responses":{"201":{"description":"API key (plaintext shown once)","content":{"application/json":{"schema":{"type":"object","properties":{"apiKey":{"type":"string"},"id":{"type":"string"},"label":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"isTest":{"type":"boolean"},"expiresAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}}}}},"401":{"description":"Missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Scope ceiling exceeded (SCOPE_CEILING_EXCEEDED), a test key minting a live key (TEST_MODE_NOT_ALLOWED), or the key lacks the admin scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/platform/keys/{keyId}/rotate":{"post":{"tags":["Platform"],"summary":"Rotate one of your keys with a grace overlap (new key shown once)","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.","parameters":[{"name":"keyId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"graceHours":{"type":"number","minimum":0,"maximum":168,"default":24}}}}}},"responses":{"201":{"description":"New key (plaintext shown once) + old-key grace deadline","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Key already revoked or expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the admin scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Key not found (or belongs to another platform)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/platform/keys/{keyId}":{"delete":{"tags":["Platform"],"summary":"Revoke one of your keys","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.","parameters":[{"name":"keyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revocation outcome","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"revoked":{"type":"boolean"},"alreadyRevoked":{"type":"boolean"}}}}}},"401":{"description":"Missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks the admin scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Key not found (or belongs to another platform)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users":{"get":{"tags":["Scores"],"summary":"List YOUR OWN subjects (query + export your book of business)","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.","parameters":[{"name":"scoreMin","in":"query","required":false,"description":"Only subjects with `lastValidScore` ≥ this (0–100).","schema":{"type":"number","minimum":0,"maximum":100}},{"name":"scoreMax","in":"query","required":false,"description":"Only subjects with `lastValidScore` ≤ this (0–100).","schema":{"type":"number","minimum":0,"maximum":100}},{"name":"band","in":"query","required":false,"description":"Only subjects in this reliability band (equivalent to the band's score range).","schema":{"type":"string","enum":["Excellent","Good","Fair","Building","Unproven","High Risk"]}},{"name":"hasScore","in":"query","required":false,"description":"true = only subjects whose score has been computed; false = only those still awaiting a first score. Cannot be combined with `scoreMin`/`scoreMax`/`band` (contradictory: 400).","schema":{"type":"boolean"}},{"name":"scoreFrozen","in":"query","required":false,"description":"true = only subjects whose score is frozen by a velocity flag; false = only unfrozen.","schema":{"type":"boolean"}},{"name":"subjectType","in":"query","required":false,"description":"Filter to people, agents or organizations.","schema":{"type":"string","enum":["PERSON","AGENT","ORGANIZATION"]}},{"name":"activeSince","in":"query","required":false,"description":"Only subjects with ≥1 of YOUR events at/after this ISO instant.","schema":{"type":"string","format":"date-time"}},{"name":"registeredSince","in":"query","required":false,"description":"Only subjects that first appeared in the ledger at/after this ISO instant (inclusive).","schema":{"type":"string","format":"date-time"}},{"name":"registeredBefore","in":"query","required":false,"description":"Only subjects that first appeared in the ledger BEFORE this ISO instant (exclusive).","schema":{"type":"string","format":"date-time"}},{"name":"hasVerifiedEvents","in":"query","required":false,"description":"true = only subjects with ≥1 of your VERIFIED events; false = only those with none.","schema":{"type":"boolean"}},{"name":"minVerifiedEvents","in":"query","required":false,"description":"Only subjects with at least this many of YOUR verified events.","schema":{"type":"integer","minimum":1}},{"name":"sort","in":"query","required":false,"description":"Sort key. Cursor-stable keys are score/registered/externalId; `lastActivity` is accepted for forward-compatibility.","schema":{"type":"string","enum":["score","lastActivity","registered","externalId"]}},{"name":"order","in":"query","required":false,"description":"Sort direction (defaults: desc for score/lastActivity, asc otherwise).","schema":{"type":"string","enum":["asc","desc"]}},{"name":"format","in":"query","required":false,"description":"'json' (default, cursor-paginated) or 'csv' (whole-book export, row-capped with an explicit truncation marker).","schema":{"type":"string","enum":["json","csv"]}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"JSON page size (1–1000, default 50). Ignored for CSV.","schema":{"type":"integer","minimum":1,"maximum":1000}}],"responses":{"200":{"description":"A page of your subjects (JSON by default; CSV stream when format=csv)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SubjectSummary"}},"nextCursor":{"type":"string","nullable":true,"description":"Pass as `cursor` to fetch the next page; `null` when there is no more data."}}}},"text/csv":{"schema":{"type":"string","example":"externalId,subjectType,finalScore,scoreBand,scoreFrozen,verificationDepth,eventCount,verifiedEventCount,lastActivityAt,registeredAt,computedAt\r\nuser-42,PERSON,72,Good,false,0.8,14,11,2026-07-20T12:00:00.000Z,2026-01-04T09:00:00.000Z,2026-07-20T12:00:05.000Z\r\n"}}}},"400":{"description":"Invalid query (BOOK_QUERY_INVALID / BOOK_FORMAT_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks scores:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/summary":{"get":{"tags":["Scores"],"summary":"Size and shape a segment of your book (counts, not rows)","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.","parameters":[{"name":"scoreMin","in":"query","required":false,"schema":{"type":"number","minimum":0,"maximum":100}},{"name":"scoreMax","in":"query","required":false,"schema":{"type":"number","minimum":0,"maximum":100}},{"name":"band","in":"query","required":false,"schema":{"type":"string","enum":["Excellent","Good","Fair","Building","Unproven","High Risk"]}},{"name":"hasScore","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"scoreFrozen","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"subjectType","in":"query","required":false,"schema":{"type":"string","enum":["PERSON","AGENT","ORGANIZATION"]}},{"name":"activeSince","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"registeredSince","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"registeredBefore","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"hasVerifiedEvents","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"minVerifiedEvents","in":"query","required":false,"schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Counts and score shape for the matched segment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookSummary"}}}},"400":{"description":"Invalid query (BOOK_QUERY_INVALID / BOOK_FORMAT_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks scores:read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/scores":{"post":{"tags":["Scores"],"summary":"Batch score read (up to 100 users in one call)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["userIds"],"properties":{"userIds":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100}}}}}},"responses":{"200":{"description":"Scores","content":{"application/json":{"schema":{"type":"object","properties":{"scores":{"type":"array","items":{"type":"object","additionalProperties":true}},"count":{"type":"integer"},"formulaVersion":{"type":"string"}}}}}},"400":{"description":"Invalid batch request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/score":{"get":{"tags":["Scores"],"summary":"Current score","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Score","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScoreResult"}}}},"404":{"description":"No score yet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/score/history":{"get":{"tags":["Scores"],"summary":"Score snapshot history","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}}],"responses":{"200":{"description":"History","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ScoreResult"}},"count":{"type":"integer"},"nextCursor":{"type":"string","nullable":true}}}}}}}}},"/api/v1/users/{id}/score/explain":{"get":{"tags":["Scores"],"summary":"Plain-language score breakdown (with adverse-action reason codes)","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`.**","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Explanation","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/v1/users/{id}/reliability":{"get":{"tags":["Scores"],"summary":"Reliability at dispatch: the compact hot-path record read","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"context","in":"query","required":false,"schema":{"type":"string","enum":["dispatch"],"default":"dispatch"},"description":"Which compact projection to serve. Only `dispatch` today."}],"responses":{"200":{"description":"Compact reliability read","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"properties":{"userId":{"type":"string"},"context":{"type":"string","example":"dispatch"},"score":{"type":"number","nullable":true,"example":73.33},"band":{"type":"string","nullable":true,"example":"Good"},"confidence":{"type":"number","nullable":true,"example":0.62},"scoreFrozen":{"type":"boolean"},"evidence":{"type":"object","properties":{"totalOutcomes":{"type":"integer","example":41},"verifiedOutcomes":{"type":"integer","example":33}}},"noShowRate":{"type":"number","nullable":true,"example":0.024},"onTimeRate":{"type":"number","nullable":true,"example":0.91},"daysSinceLastEvent":{"type":"integer","nullable":true,"example":3},"topFactors":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"direction":{"type":"string","enum":["adverse","supporting"]},"title":{"type":"string"},"contribution":{"type":"number"}}}},"note":{"type":"string"},"disclosures":{"type":"array","items":{"type":"string"}}}}}}},"400":{"description":"Unknown context (DISPATCH_CONTEXT_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/trust-summary":{"get":{"tags":["Scores"],"summary":"Evidence-based trust explanation (summary, strengths, risks)","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).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"narrative","in":"query","required":false,"schema":{"type":"string","enum":["1"]},"description":"Set to 1 to request the optional advisory AI narrative."}],"responses":{"200":{"description":"Trust explanation","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"properties":{"summary":{"type":"string"},"strengths":{"type":"array","items":{"type":"string"}},"risks":{"type":"array","items":{"type":"string"}},"advisory":{"type":"string"}}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/score/delta":{"get":{"tags":["Scores"],"summary":"Factor-level explanation of the last score change","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Score delta","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/earnings":{"get":{"tags":["Earnings"],"summary":"Verified earnings attestation (monthly breakdown + stability metrics)","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"months","in":"query","required":false,"description":"Trailing window length in months (1–120). Ignored when `from` is supplied. Default 12.","schema":{"type":"integer","minimum":1,"maximum":120,"default":12}},{"name":"from","in":"query","required":false,"description":"Window start (ISO-8601). Overrides `months`.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"description":"Window end (ISO-8601). Defaults to now.","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Verified earnings","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifiedEarnings"}}}},"400":{"description":"Invalid window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/earnings/summary":{"get":{"tags":["Earnings"],"summary":"Compact earnings summary (trailing 12m total, median monthly, volatility, coverage)","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`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"months","in":"query","required":false,"description":"Trailing window length in months (1–120). Ignored when `from` is supplied. Default 12.","schema":{"type":"integer","minimum":1,"maximum":120,"default":12}},{"name":"from","in":"query","required":false,"description":"Window start (ISO-8601). Overrides `months`.","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"description":"Window end (ISO-8601). Defaults to now.","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Earnings summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EarningsSummary"}}}},"400":{"description":"Invalid window","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/earnings/credential":{"post":{"tags":["Earnings"],"summary":"Mint a signed Verified Earnings Credential (W3C VC-JWT)","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"months":{"type":"integer","minimum":1,"maximum":120,"default":12,"description":"Trailing window length in months."},"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"ttlSeconds":{"type":"integer","minimum":60,"description":"Credential lifetime; bounded by the issuer TTL."}}}}}},"responses":{"201":{"description":"Signed earnings credential","content":{"application/json":{"schema":{"type":"object","properties":{"format":{"type":"string","enum":["jwt_vc_json"]},"credentialVc":{"type":"string","description":"Compact VC-JWT."},"credentialType":{"type":"string","enum":["CreddaEarningsCredential"]},"issuer":{"type":"string","example":"did:web:api.credda.io"},"kid":{"type":"string"},"earningsVersion":{"type":"string","example":"1.0"},"claims":{"type":"object","additionalProperties":true},"issuedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"didDocument":{"type":"string"},"trustRegistry":{"type":"string"},"statusList":{"type":"string"}}}}}},"400":{"description":"Invalid window or ttl","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Test-mode keys cannot issue credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/professional-record":{"get":{"tags":["Professional Record"],"summary":"Worker-owned summary of a verified work record (résumé-shaped)","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Professional record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfessionalRecord"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/professional-record/credential":{"post":{"tags":["Professional Record"],"summary":"Mint a signed Professional Record Credential + an \"Add to LinkedIn\" link","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"ttlSeconds":{"type":"integer","minimum":60,"description":"Credential lifetime; bounded by the issuer TTL."}}}}}},"responses":{"201":{"description":"Signed professional record credential","content":{"application/json":{"schema":{"type":"object","properties":{"format":{"type":"string","enum":["jwt_vc_json"]},"credentialVc":{"type":"string","description":"Compact VC-JWT."},"credentialType":{"type":"string","enum":["CreddaProfessionalRecordCredential"]},"issuer":{"type":"string","example":"did:web:api.credda.io"},"kid":{"type":"string"},"professionalRecordVersion":{"type":"string","example":"1.1"},"claims":{"type":"object","additionalProperties":true},"linkedin":{"type":"object","properties":{"addToProfileUrl":{"type":"string","description":"LinkedIn \"Add to profile\" certification deep link."},"certUrl":{"type":"string","description":"Public, verifiable proof the \"Show credential\" link resolves to."},"certId":{"type":"string"},"note":{"type":"string"}}},"issuedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"didDocument":{"type":"string"},"trustRegistry":{"type":"string"},"statusList":{"type":"string"}}}}}},"400":{"description":"Invalid ttl","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Test-mode keys cannot issue credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/verified-profile":{"get":{"tags":["Verified Profile"],"summary":"Verified-profile measure (verification depth of the qualifications record)","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Verified profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifiedProfile"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/career-export":{"get":{"tags":["Professional Record"],"summary":"Portable professional record in the open JSON Resume schema","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"format","in":"query","required":false,"description":"Open serialisation: 'jsonresume' (default) or 'jsonld' (schema.org Person).","schema":{"type":"string","enum":["jsonresume","jsonld"]}}],"responses":{"200":{"description":"JSON Resume document (or schema.org JSON-LD when format=jsonld), each item flagged verified vs self-reported","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Unsupported format (CAREER_FORMAT_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/reliability-report":{"get":{"tags":["Professional Record"],"summary":"One-call worker reliability report (decision-support dossier)","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"recent","in":"query","required":false,"description":"Recent outcomes to include (1–50, default 10).","schema":{"type":"integer","minimum":1,"maximum":50,"default":10}},{"name":"benchmark","in":"query","required":false,"description":"Set to 1 to attach the coarse quartile-grain benchmark.","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"Consolidated reliability report","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"properties":{"reliabilityReportVersion":{"type":"string","example":"1.0"},"reliability":{"type":"object","additionalProperties":true},"metrics":{"type":"object","additionalProperties":true},"verifiedExperience":{"type":"object","additionalProperties":true},"topFactors":{"type":"array","items":{"type":"object","additionalProperties":true}},"recentOutcomes":{"type":"array","items":{"type":"object","properties":{"eventType":{"type":"string"},"stake":{"type":"string"},"verified":{"type":"boolean"},"source":{"type":"string","enum":["verified","self_reported"]},"occurredAt":{"type":"string","format":"date-time"}}}},"benchmark":{"type":"object","nullable":true,"additionalProperties":true},"disclosures":{"type":"array","items":{"type":"string"}}}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/qualifications":{"post":{"tags":["Verified Profile"],"summary":"Record a qualification claim (education / skill / certification / employment)","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["category"],"properties":{"category":{"type":"string","enum":["education","skill","certification","employment"],"description":"Which part of the qualifications record this claim belongs to."},"label":{"type":"string","maxLength":200,"description":"Free-text claim label (e.g. \"BSc Computer Science\"). Display only, never scored or ranked."},"issuer":{"type":"string","maxLength":200,"description":"Free-text institution/employer/issuing body. Display only, never scored or ranked."},"jurisdiction":{"type":"string","maxLength":32,"example":"US-TX","description":"Where this credential applies, ISO-3166 flavoured (`US`, `US-TX`, `GB`, `GB-SCT`, `DE-BY`). Normalised to upper case with hyphen separators, but deliberately NOT validated against a fixed country/region list: an unrecognised code is recorded rather than rejected, because being unable to record a real credential is worse than recording an unfamiliar code. A property of the CREDENTIAL, never of the person: never stored on the subject, never indexed, never queryable, never ranked."},"reference":{"type":"string","maxLength":100,"example":"TECL-28417","description":"The issuing body's own identifier for the credential: a licence, registration or certificate number, as printed on it, so a reader can check the claim against that body's register. Display only. Credda does not resolve it and asserts nothing about whether it is current."},"verifiedBy":{"type":"string","maxLength":200,"description":"The third-party witness confirming the claim. Required for the claim to count as verified; must not be the subject."}}}}}},"responses":{"201":{"description":"Qualification recorded","content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"eventId":{"type":"string"},"category":{"type":"string","enum":["education","skill","certification","employment"]},"eventType":{"type":"string","enum":["EDUCATION_VERIFIED","SKILL_VERIFIED","CERTIFICATION_VERIFIED","EMPLOYMENT_VERIFIED"]},"isVerified":{"type":"boolean","description":"True only when a genuine third-party witness was supplied."},"jurisdiction":{"type":"string","nullable":true,"description":"The normalised jurisdiction as stored, so you can see exactly what was recorded. Null when not supplied."},"reference":{"type":"string","nullable":true,"description":"The issuing body's reference as stored. Null when not supplied."},"verificationNote":{"type":"string","nullable":true,"description":"Why the claim was recorded as self-attested, when applicable."},"note":{"type":"string"}}}}}},"400":{"description":"Invalid category","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/qualifications/import":{"post":{"tags":["Verified Profile"],"summary":"Bulk-import a claimed professional record (education / employment / skills / certifications)","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["claims"],"properties":{"claims":{"type":"array","minItems":1,"maxItems":100,"description":"The claims to record. Each is validated exactly as the single-claim endpoint validates its body.","items":{"type":"object","required":["category"],"properties":{"category":{"type":"string","enum":["education","skill","certification","employment"],"description":"Which part of the qualifications record this claim belongs to."},"label":{"type":"string","maxLength":200,"description":"Free-text claim label. Display only, never scored or ranked."},"issuer":{"type":"string","maxLength":200,"description":"Free-text institution/employer/issuing body. Display only, never scored or ranked."},"jurisdiction":{"type":"string","maxLength":32,"example":"US-TX","description":"Where this credential applies. A property of the credential, never of the person; never scored or ranked."},"reference":{"type":"string","maxLength":100,"example":"TECL-28417","description":"The issuing body's own identifier for the credential. Display only; Credda does not resolve it."},"verifiedBy":{"type":"string","maxLength":200,"description":"The third-party witness confirming THIS claim. Required for the claim to count as verified; must not be the subject."}}}}}}}}},"responses":{"200":{"description":"Import summary (partial success)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QualificationImportResult"}}}},"400":{"description":"Malformed body, over the per-import cap, or an invalid claim","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/projects":{"get":{"tags":["Professional Record"],"summary":"List the subject's personal projects (verified vs self-reported)","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`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Personal projects","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PersonalProjects"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Professional Record"],"summary":"Record a personal project (self-reported by default)","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","url"],"properties":{"title":{"type":"string","minLength":1,"maxLength":200,"description":"Project title. Display only, never scored."},"url":{"type":"string","maxLength":2048,"description":"Absolute http(s) link to a public host. Stored as text; never fetched."},"description":{"type":"string","maxLength":1000,"description":"Optional free-text description. Display only."},"verifiedBy":{"type":"string","maxLength":200,"description":"The third-party witness confirming the project. Required for the project to count as verified; must not be the subject."}}}}}},"responses":{"201":{"description":"Project recorded","content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string"},"eventId":{"type":"string"},"title":{"type":"string"},"url":{"type":"string"},"isVerified":{"type":"boolean","description":"True only when a genuine third-party witness was supplied."},"verificationNote":{"type":"string","nullable":true,"description":"Why the project was recorded as self-reported, when applicable."},"note":{"type":"string"}}}}}},"400":{"description":"Invalid project (missing title, or url is not a public http(s) link)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/credential-offer":{"post":{"tags":["Wallets"],"summary":"Mint an OID4VCI Credential Offer for this subject","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"credentialConfigurationIds":{"type":"array","items":{"type":"string","enum":["CreddaTrustCredential_sd_jwt_vc","CreddaTrustCredential_jwt_vc_json","CreddaEarningsCredential_sd_jwt_vc","CreddaEarningsCredential_jwt_vc_json","CreddaProfessionalRecordCredential_sd_jwt_vc","CreddaProfessionalRecordCredential_jwt_vc_json","CreddaDeliveryReceiptCredential_sd_jwt_vc","CreddaDeliveryReceiptCredential_jwt_vc_json","CreddaAgentDeliveryCredential_sd_jwt_vc","CreddaAgentDeliveryCredential_jwt_vc_json","CreddaBusinessRecordCredential_sd_jwt_vc","CreddaBusinessRecordCredential_jwt_vc_json"]},"description":"Which credential configurations the offer covers. Defaults to the SD-JWT trust credential."},"scope":{"type":"string","enum":["minimal","band","full"],"default":"full"}}}}}},"responses":{"201":{"description":"Credential offer","content":{"application/json":{"schema":{"type":"object","properties":{"credentialOffer":{"type":"object","additionalProperties":true},"credentialOfferUri":{"type":"string","example":"openid-credential-offer://?credential_offer=%7B…%7D"},"expiresIn":{"type":"integer","description":"Lifetime of the pre-authorized code, in seconds."},"scope":{"type":"string","enum":["minimal","band","full"]},"credentialIssuer":{"type":"string"},"issuerMetadata":{"type":"string"}}}}}},"400":{"description":"Unknown credential configuration or scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Test-mode keys cannot issue credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/score/components":{"get":{"tags":["Scores"],"summary":"Modular score components (Reliability, Timeliness, Trustworthiness, Verification Confidence, Consistency, Momentum)","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Score components","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/benchmark":{"get":{"tags":["Benchmarks"],"summary":"Where this subject sits within a cohort (percentile)","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"dimension","in":"query","required":false,"schema":{"type":"string","enum":["all","subjectType","verificationDepthBand","activityVolumeBand","tenureBand"]},"description":"Cohort dimension. Default `all`."}],"responses":{"200":{"description":"Subject benchmark (may be `available:false`)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Unknown cohort dimension","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/timeline":{"get":{"tags":["Scores"],"summary":"Unified chronological feed of events + score changes","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Timeline","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":true}},"count":{"type":"integer"},"nextCursor":{"type":"string","nullable":true}}}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/score/project":{"post":{"tags":["Scores"],"summary":"What-if score projection (read-only, never writes a snapshot)","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["events"],"properties":{"events":{"type":"array","minItems":1,"maxItems":20,"description":"Hypothetical prospective events to project onto the ledger.","items":{"type":"object","required":["eventType"],"properties":{"eventType":{"type":"string","enum":["TRANSACTION_COMPLETED","TRANSACTION_DISPUTED","TRANSACTION_CANCELLED","REVIEW_VERIFIED","CONTRACT_FULFILLED","CONTRACT_CANCELLED","CONTRACT_BREACHED","DISPUTE_FILED","DISPUTE_RESOLVED_FOR_USER","DISPUTE_RESOLVED_AGAINST_USER"]},"stakeLevel":{"type":"string","enum":["HIGH","MEDIUM","LOW"],"default":"MEDIUM"},"platformTier":{"type":"string","enum":["ENTERPRISE","GROWTH","STARTER","SELF_REPORTED"],"default":"GROWTH"},"isVerified":{"type":"boolean","default":true},"daysLate":{"type":"integer","minimum":0,"default":0},"transactionValue":{"type":"number","minimum":0,"nullable":true}}}}}}}}},"responses":{"200":{"description":"Projection","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Invalid projection request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"User not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/documents/analyze":{"post":{"tags":["Scores"],"summary":"Résumé / document advisory: who confirms it, and how it affects the score (writes nothing)","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["claims"],"properties":{"claims":{"type":"array","minItems":1,"maxItems":50,"description":"The structured outcome claims to advise on.","items":{"type":"object","required":["eventType"],"properties":{"eventType":{"type":"string","enum":["TRANSACTION_COMPLETED","CONTRACT_FULFILLED","REVIEW_VERIFIED","DISPUTE_RESOLVED_FOR_USER","TRANSACTION_CANCELLED","CONTRACT_CANCELLED","CONTRACT_BREACHED","TRANSACTION_DISPUTED"]},"label":{"type":"string","maxLength":200,"description":"Your own reference label, never scored."},"stakeLevel":{"type":"string","enum":["HIGH","MEDIUM","LOW"],"default":"MEDIUM"},"transactionValue":{"type":"number","minimum":0,"nullable":true},"daysLate":{"type":"integer","minimum":0,"default":0},"counterpartyRef":{"type":"string","maxLength":200,"description":"Who can confirm this. Its PRESENCE determines whether the claim can count as verified; the value is opaque here."}}}}}}}}},"responses":{"200":{"description":"Per-claim advice + a two-scenario score projection + witness guide","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Invalid request (provide 1–50 structured claims)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/users/{id}/platforms":{"get":{"tags":["Scores"],"summary":"Contributing platforms","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Platforms","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/v1/users/{id}/risk":{"get":{"tags":["Scores"],"summary":"Advisory anti-gaming risk signals (never affects the score)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Risk signals","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/v1/users/{id}/share-token":{"post":{"tags":["Verify"],"summary":"Mint/rotate a public trust-badge token","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Token + embed snippet","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}},"delete":{"tags":["Verify"],"summary":"Revoke the share token","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revoked"}}}},"/api/v1/usage":{"get":{"tags":["Scores"],"summary":"Your API usage vs. tier quota","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.","parameters":[{"name":"days","in":"query","required":false,"description":"Trailing window in days (default 7, max 400). Mutually exclusive with `from`/`to`.","schema":{"type":"integer","minimum":1,"maximum":400}},{"name":"from","in":"query","required":false,"description":"Inclusive range start (ISO date, YYYY-MM-DD). Requires `to`; clamped to the 400-day history window (completed days beyond the 90-day live retention are served from durable daily rollups).","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":false,"description":"Inclusive range end (ISO date, YYYY-MM-DD). Requires `from`.","schema":{"type":"string","format":"date"}},{"name":"format","in":"query","required":false,"description":"Response format: 'json' (default) or 'csv' (flat statement, `Content-Disposition: attachment`).","schema":{"type":"string","enum":["json","csv"]}}],"responses":{"200":{"description":"Usage summary (JSON by default; CSV statement when format=csv)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"properties":{"endpoints":{"type":"array","items":{"type":"object","properties":{"endpoint":{"type":"string","example":"GET /api/v1/users/{id}/score"},"total":{"type":"integer"}}}}}}},"text/csv":{"schema":{"type":"string","example":"row,date,endpoint,total,ok,clientError,serverError\r\nday,2026-07-01,,120,118,2,0\r\ntotal,,,120,118,2,0\r\nendpoint,,GET /api/v1/users/{id}/score,80,,,\r\n"}}}},"400":{"description":"Invalid window (USAGE_WINDOW_CONFLICT / USAGE_RANGE_INVALID / USAGE_FORMAT_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/usage/quota":{"get":{"tags":["Scores"],"summary":"Quota transparency: remaining monthly calls + reset","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.","responses":{"200":{"description":"Quota state","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"properties":{"rateLimitPerMin":{"type":"integer","example":600},"unlimited":{"type":"boolean"},"cap":{"type":"integer","nullable":true,"example":100000},"used":{"type":"integer","example":812},"remaining":{"type":"integer","nullable":true,"example":99188},"usedRatio":{"type":"number","nullable":true,"example":0.0081},"resetAt":{"type":"string","format":"date-time"},"secondsUntilReset":{"type":"integer","example":1209600}}}}}}}}},"/api/v1/usage/meters":{"get":{"tags":["Scores"],"summary":"Usage as metered-billing meters (Stripe Billing Meters shape)","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.","parameters":[{"name":"days","in":"query","required":false,"description":"Trailing window in days (default 7, max 400). Mutually exclusive with `from`/`to`.","schema":{"type":"integer","minimum":1,"maximum":400}},{"name":"from","in":"query","required":false,"description":"Inclusive range start (ISO date, YYYY-MM-DD). Requires `to`; clamped to the 400-day history window (completed days beyond the 90-day live retention are served from durable daily rollups).","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":false,"description":"Inclusive range end (ISO date, YYYY-MM-DD). Requires `from`.","schema":{"type":"string","format":"date"}},{"name":"format","in":"query","required":false,"description":"Response format: 'json' (default) or 'csv' (flat statement, `Content-Disposition: attachment`).","schema":{"type":"string","enum":["json","csv"]}}],"responses":{"200":{"description":"Meter rows (JSON by default; CSV attachment when format=csv)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"properties":{"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"},"meters":{"type":"array","items":{"type":"object","properties":{"meter":{"type":"string","example":"credda_api_requests"},"dimension":{"type":"string","enum":["total","status_class","endpoint"]},"value":{"type":"string","example":"all"},"quantity":{"type":"integer"}}}}}}},"text/csv":{"schema":{"type":"string","example":"meter,dimension,value,quantity,windowFrom,windowTo\r\ncredda_api_requests,total,all,120,2026-07-01,2026-07-07\r\ncredda_api_requests,status_class,ok,118,2026-07-01,2026-07-07\r\n"}}}},"400":{"description":"Invalid window (USAGE_WINDOW_CONFLICT / USAGE_RANGE_INVALID / USAGE_FORMAT_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/analytics/events":{"get":{"tags":["Analytics"],"summary":"Event volume time-series (by day + by type + verified & confirmed share)","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.","parameters":[{"name":"days","in":"query","required":false,"description":"Trailing window in days (default 30, max 365). Mutually exclusive with `from`/`to`.","schema":{"type":"integer","minimum":1,"maximum":365}},{"name":"from","in":"query","required":false,"description":"Inclusive range start (ISO date, YYYY-MM-DD). Requires `to`.","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":false,"description":"Inclusive range end (ISO date, YYYY-MM-DD). Requires `from`.","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Event analytics","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"properties":{"totals":{"type":"object","properties":{"total":{"type":"integer"},"verified":{"type":"integer"},"confirmed":{"type":"integer","description":"Events a distinct counterparty confirmed via a one-time token. Subset of `verified`."},"verifiedShare":{"type":"number","nullable":true},"confirmedShare":{"type":"number","nullable":true,"description":"confirmed / total, 2dp; null when the window has no events."}}},"daily":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","format":"date"},"total":{"type":"integer"},"verified":{"type":"integer"},"confirmed":{"type":"integer"},"verifiedShare":{"type":"number","nullable":true},"confirmedShare":{"type":"number","nullable":true}}}},"byType":{"type":"array","items":{"type":"object","properties":{"eventType":{"type":"string"},"total":{"type":"integer"},"verified":{"type":"integer"},"confirmed":{"type":"integer"},"verifiedShare":{"type":"number","nullable":true},"confirmedShare":{"type":"number","nullable":true}}}}}}}}},"400":{"description":"Invalid window (ANALYTICS_WINDOW_CONFLICT / ANALYTICS_RANGE_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/analytics/scores":{"get":{"tags":["Analytics"],"summary":"Score distribution + movement across your subjects","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.","parameters":[{"name":"days","in":"query","required":false,"description":"Trailing window in days (default 30, max 365). Mutually exclusive with `from`/`to`.","schema":{"type":"integer","minimum":1,"maximum":365}},{"name":"from","in":"query","required":false,"description":"Inclusive range start (ISO date, YYYY-MM-DD). Requires `to`.","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":false,"description":"Inclusive range end (ISO date, YYYY-MM-DD). Requires `from`.","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Score analytics","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"properties":{"formulaVersion":{"type":"string"},"scoredSubjects":{"type":"integer"},"central":{"type":"object","properties":{"median":{"type":"number","nullable":true},"mean":{"type":"number","nullable":true}}},"bandDistribution":{"type":"array","items":{"type":"object","properties":{"band":{"type":"string"},"minScore":{"type":"integer"},"count":{"type":"integer"},"share":{"type":"number","nullable":true}}}},"movement":{"type":"object","properties":{"up":{"type":"integer"},"down":{"type":"integer"},"unchanged":{"type":"integer"},"subjectsMoved":{"type":"integer"},"subjectsRecomputed":{"type":"integer"}}}}}}}},"400":{"description":"Invalid window (ANALYTICS_WINDOW_CONFLICT / ANALYTICS_RANGE_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/disputes/{id}/resolve":{"patch":{"tags":["Disputes"],"summary":"Resolve a dispute","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["outcome"],"properties":{"outcome":{"type":"string","enum":["FOR_USER","AGAINST_USER"]}}}}}},"responses":{"200":{"description":"Resolved"},"403":{"description":"Not your event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already resolved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/webhooks":{"get":{"tags":["Webhooks"],"summary":"List your webhooks","responses":{"200":{"description":"Webhooks","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}}}}}}}}},"post":{"tags":["Webhooks"],"summary":"Subscribe an HTTPS endpoint (secret returned once)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","minItems":1,"items":{"type":"string","enum":["score.updated","score.band_changed","dispute.resolved","monitor.triggered","policy.threshold_crossed","usage.quota_warning","import.completed","screening.completed","confirmation.awaiting_response","confirmation.expiring_soon"]}},"description":{"type":"string"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"webhook":{"$ref":"#/components/schemas/Webhook"},"secret":{"type":"string","description":"Signing secret, shown ONCE."}}}}}},"400":{"description":"Invalid url/events","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/webhooks/{id}":{"get":{"tags":["Webhooks"],"summary":"Fetch one webhook","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Webhook","content":{"application/json":{"schema":{"type":"object","properties":{"webhook":{"$ref":"#/components/schemas/Webhook"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Webhooks"],"summary":"Update url/events/description/isActive","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"type":"object","properties":{"webhook":{"$ref":"#/components/schemas/Webhook"}}}}}}}},"delete":{"tags":["Webhooks"],"summary":"Delete a webhook","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"}}}},"/api/v1/webhooks/{id}/test":{"post":{"tags":["Webhooks"],"summary":"Send a synthetic signed delivery","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Delivery result","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/v1/webhooks/deliveries":{"get":{"tags":["Webhooks"],"summary":"Recent outbound events across ALL your endpoints (sample data)","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.","parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"eventType","in":"query","required":false,"description":"Filter to one or more event types (repeat the param or comma-separate). An unknown type is a 400, not an empty result.","schema":{"type":"string","enum":["score.updated","score.band_changed","dispute.resolved","monitor.triggered","policy.threshold_crossed","usage.quota_warning","import.completed","screening.completed","confirmation.awaiting_response","confirmation.expiring_soon"]}}],"responses":{"200":{"description":"Recent events","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RecentWebhookEvent"}},"nextCursor":{"type":"string","nullable":true},"source":{"type":"string","enum":["deliveries","examples"],"description":"`examples` means the catalog fallback was used; nothing has been delivered yet."}}}}}},"400":{"description":"Unknown eventType (BAD_REQUEST)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/webhooks/{id}/deliveries":{"get":{"tags":["Webhooks"],"summary":"Recent delivery attempts","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Deliveries","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":true}},"nextCursor":{"type":"string","nullable":true,"description":"Pass as `cursor` to fetch the next page; `null` when there is no more data."}}}}}}}}},"/api/v1/webhooks/{id}/deliveries/{deliveryId}/replay":{"post":{"tags":["Webhooks"],"summary":"Replay a past delivery","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"deliveryId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Replay outcome","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["replayed"]},"success":{"type":"boolean"},"statusCode":{"type":"integer","nullable":true},"error":{"type":"string","nullable":true}}}}}},"404":{"description":"Delivery not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Payload not retained","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/monitors":{"get":{"tags":["Monitors"],"summary":"List your score monitors","parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Monitors","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Monitor"}},"nextCursor":{"type":"string","nullable":true,"description":"Pass as `cursor` to fetch the next page; `null` when there is no more data."}}}}}}}},"post":{"tags":["Monitors"],"summary":"Create a score monitor on one of your users","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.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["userId"],"properties":{"userId":{"type":"string","description":"The platform's external user id (externalId)."},"belowScore":{"type":"number","minimum":0,"maximum":100},"aboveScore":{"type":"number","minimum":0,"maximum":100},"onBandChange":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"monitor":{"$ref":"#/components/schemas/Monitor"}}}}}},"400":{"description":"No condition provided / validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown userId (USER_NOT_FOUND)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Active monitor limit reached for the plan tier (MONITOR_LIMIT_REACHED; `details` carries `{limit, tier}`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/monitors/{id}":{"get":{"tags":["Monitors"],"summary":"Fetch one monitor","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Monitor","content":{"application/json":{"schema":{"type":"object","properties":{"monitor":{"$ref":"#/components/schemas/Monitor"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Monitors"],"summary":"Update thresholds / onBandChange / isActive","description":"Set `belowScore`/`aboveScore` to null to clear a threshold. The updated monitor must keep at least one condition.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"belowScore":{"type":"number","nullable":true,"minimum":0,"maximum":100},"aboveScore":{"type":"number","nullable":true,"minimum":0,"maximum":100},"onBandChange":{"type":"boolean"},"isActive":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"type":"object","properties":{"monitor":{"$ref":"#/components/schemas/Monitor"}}}}}},"400":{"description":"Update would leave the monitor with no condition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Monitors"],"summary":"Delete a monitor (hard delete; it is config, not ledger data)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/notification-channels":{"get":{"tags":["Notification Channels"],"summary":"List your notification channels","parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Notification channels","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/NotificationChannel"}},"nextCursor":{"type":"string","nullable":true,"description":"Pass as `cursor` to fetch the next page; `null` when there is no more data."}}}}}}}},"post":{"tags":["Notification Channels"],"summary":"Create a Slack or email notification channel","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).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","target","name"],"properties":{"type":{"type":"string","enum":["slack","email"]},"target":{"type":"string","description":"A hooks.slack.com incoming-webhook URL (slack) or an email address (email)."},"name":{"type":"string","maxLength":120,"description":"A label for your own dashboards."}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"channel":{"$ref":"#/components/schemas/NotificationChannel"}}}}}},"400":{"description":"Invalid type/target (NOTIFICATION_CHANNEL_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Per-platform channel limit reached (NOTIFICATION_CHANNEL_LIMIT_REACHED; `details` carries `{limit}`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/notification-channels/{id}":{"get":{"tags":["Notification Channels"],"summary":"Fetch one channel","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Notification channel","content":{"application/json":{"schema":{"type":"object","properties":{"channel":{"$ref":"#/components/schemas/NotificationChannel"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Notification Channels"],"summary":"Delete a channel (hard delete; it is config, not ledger data)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/policies":{"get":{"tags":["Policies"],"summary":"List your threshold policies","parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Policies","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ThresholdPolicy"}},"nextCursor":{"type":"string","nullable":true,"description":"Pass as `cursor` to fetch the next page; `null` when there is no more data."}}}}}}}},"post":{"tags":["Policies"],"summary":"Create a threshold policy","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.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","metric"],"properties":{"name":{"type":"string","maxLength":120},"userId":{"type":"string","description":"The subject's externalId. Omit and set appliesToAll instead to watch all subjects."},"appliesToAll":{"type":"boolean","description":"Watch every one of your subjects. Mutually exclusive with userId."},"metric":{"type":"string","enum":["score","component","band","verified_events"]},"direction":{"type":"string","enum":["up","down","enter","leave"],"description":"up/down for numeric metrics; enter/leave for a band."},"threshold":{"type":"number","description":"0–100 for score/component; a non-negative integer for verified_events."},"component":{"type":"string","enum":["reliability","timeliness","trustworthiness","verification","consistency","momentum"],"description":"Required when metric = 'component'."},"band":{"type":"string","description":"When metric = 'band'; omit to watch any band change."}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"policy":{"$ref":"#/components/schemas/ThresholdPolicy"}}}}}},"400":{"description":"Invalid scope or condition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown userId (USER_NOT_FOUND)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Active policy limit reached for the plan tier (POLICY_LIMIT_REACHED; `details` carries `{limit, tier, mode}`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/policies/{id}":{"get":{"tags":["Policies"],"summary":"Fetch one policy","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Policy","content":{"application/json":{"schema":{"type":"object","properties":{"policy":{"$ref":"#/components/schemas/ThresholdPolicy"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Policies"],"summary":"Retune the line / rename / activate / deactivate","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":120},"direction":{"type":"string","nullable":true,"enum":["up","down","enter","leave"]},"threshold":{"type":"number","nullable":true},"component":{"type":"string","nullable":true,"enum":["reliability","timeliness","trustworthiness","verification","consistency","momentum"]},"band":{"type":"string","nullable":true},"isActive":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"type":"object","properties":{"policy":{"$ref":"#/components/schemas/ThresholdPolicy"}}}}}},"400":{"description":"Update would leave an invalid condition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Active policy limit reached (POLICY_LIMIT_REACHED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Policies"],"summary":"Delete a policy (hard delete; it is config, not ledger data)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/ingest":{"post":{"tags":["Ingest"],"summary":"Ingest YOUR payload shape via a declarative field mapping","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.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["records"],"properties":{"mapping":{"$ref":"#/components/schemas/IngestMappingDoc"},"mappingId":{"type":"string","description":"A stored mapping (POST /ingest/mappings) instead of an inline one. Mutually exclusive with `mapping`."},"records":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"object","additionalProperties":true},"description":"Your records, verbatim, any JSON object shape."}}},"example":{"mapping":{"userId":"worker.id","eventType":{"path":"status","values":{"done":"CONTRACT_FULFILLED","missed":"CONTRACT_BREACHED"}},"transactionValue":{"path":"amount_cents","transform":"cents_to_units"},"completedAt":{"path":"completed_at","transform":"iso_date"},"stakeLevel":{"path":"tier","transform":"lowercase","values":{"gold":"HIGH"},"default":"MEDIUM"},"isVerified":{"const":true},"verifiedBy":"client.email","idempotencyKey":"order_id"},"records":[{"worker":{"id":"w_42"},"status":"done","amount_cents":12500,"completed_at":"2026-03-04T10:00:00Z","tier":"Gold","client":{"email":"ops@acme.test"},"order_id":"order_00099123"}]}}}},"responses":{"200":{"description":"Processed (partial success)","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"integer"},"created":{"type":"integer"},"duplicate":{"type":"integer"},"failed":{"type":"integer"},"results":{"type":"array","items":{"$ref":"#/components/schemas/IngestResultItem"}}}}}}},"400":{"description":"Validation error, or an invalid mapping (INGEST_MAPPING_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks events:write","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown mappingId","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/ingest/mappings":{"get":{"tags":["Ingest"],"summary":"List your stored mappings","parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Mappings","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/StoredMapping"}},"nextCursor":{"type":"string","nullable":true,"description":"Pass as `cursor` to fetch the next page; `null` when there is no more data."}}}}}}}},"post":{"tags":["Ingest"],"summary":"Save a reusable named mapping","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.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","mapping"],"properties":{"name":{"type":"string","minLength":1,"maxLength":80},"description":{"type":"string","maxLength":500},"mapping":{"$ref":"#/components/schemas/IngestMappingDoc"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"mapping":{"$ref":"#/components/schemas/StoredMapping"}}}}}},"400":{"description":"Invalid mapping (INGEST_MAPPING_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A mapping with that name already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/ingest/mappings/{id}":{"get":{"tags":["Ingest"],"summary":"Fetch one stored mapping","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Mapping","content":{"application/json":{"schema":{"type":"object","properties":{"mapping":{"$ref":"#/components/schemas/StoredMapping"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Ingest"],"summary":"Delete a stored mapping (config, not ledger data; events already ingested are untouched)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/imports":{"get":{"tags":["Ingest"],"summary":"List your CSV imports (status + counts)","parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Imports","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ImportJob"}},"nextCursor":{"type":"string","nullable":true,"description":"Pass as `cursor` to fetch the next page; `null` when there is no more data."}}}}}}}},"post":{"tags":["Ingest"],"summary":"Backfill historical outcomes from a CSV","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`.","parameters":[{"name":"mappingId","in":"query","required":false,"description":"Required for a `text/csv` body; ignored when the JSON body carries `mapping`/`mappingId`.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["csv"],"properties":{"csv":{"type":"string","description":"The file contents."},"mapping":{"$ref":"#/components/schemas/IngestMappingDoc"},"mappingId":{"type":"string"}}},"example":{"csv":"worker_id,outcome,completed_on,amount,client_email,job_ref\nw_1,delivered,2025-02-03,450,ops@acme.test,legacy-job-0001\n","mapping":{"userId":"worker_id","eventType":{"path":"outcome","values":{"delivered":"CONTRACT_FULFILLED","breached":"CONTRACT_BREACHED"}},"completedAt":{"path":"completed_on","transform":"iso_date"},"transactionValue":"amount","isVerified":{"const":true},"verifiedBy":"client_email","idempotencyKey":"job_ref"}}},"text/csv":{"schema":{"type":"string"}}}},"responses":{"202":{"description":"Accepted (inline imports return already COMPLETED)","content":{"application/json":{"schema":{"type":"object","properties":{"import":{"$ref":"#/components/schemas/ImportJob"}}}}}},"400":{"description":"IMPORT_CSV_INVALID / IMPORT_TOO_MANY_ROWS / INGEST_MAPPING_INVALID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing/invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API key lacks events:write","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown mappingId","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"IMPORT_QUEUE_UNAVAILABLE: large imports cannot be accepted right now","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/imports/{id}":{"get":{"tags":["Ingest"],"summary":"Import status + counts","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Import","content":{"application/json":{"schema":{"type":"object","properties":{"import":{"$ref":"#/components/schemas/ImportJob"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/imports/{id}/errors":{"get":{"tags":["Ingest"],"summary":"Per-row failures (and warnings) so you can fix and re-upload","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":1000}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0}}],"responses":{"200":{"description":"Row errors","content":{"application/json":{"schema":{"type":"object","properties":{"import":{"$ref":"#/components/schemas/ImportJob"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ImportRowError"}},"errorCount":{"type":"integer"},"warnings":{"type":"array","items":{"type":"object","properties":{"row":{"type":"integer"},"warning":{"type":"string"},"userId":{"type":"string"}}}},"warningCount":{"type":"integer"},"truncated":{"type":"boolean","description":"True when more rows failed than the stored list retains."}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/confirmations":{"get":{"tags":["Confirmations"],"summary":"List your confirmation requests","parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["PENDING","CONFIRMED","DECLINED","EXPIRED","CANCELLED"]},"description":"Filter to one lifecycle state."}],"responses":{"200":{"description":"Confirmation requests","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ConfirmationRequest"}},"nextCursor":{"type":"string","nullable":true,"description":"Pass as `cursor` to fetch the next page; `null` when there is no more data."}}}}}}}},"post":{"tags":["Confirmations"],"summary":"Propose an outcome for a counterparty to confirm","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.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional; makes a retried create return the original request + token instead of a duplicate."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["userId","eventType","counterpartyRef"],"properties":{"userId":{"type":"string","description":"The subject's externalId (the platform's own id)."},"eventType":{"type":"string","enum":["TRANSACTION_COMPLETED","CONTRACT_FULFILLED","REVIEW_VERIFIED","DISPUTE_RESOLVED_FOR_USER","TRANSACTION_CANCELLED","CONTRACT_CANCELLED","CONTRACT_BREACHED","TRANSACTION_DISPUTED"],"description":"The outcome to have confirmed; applied verbatim on confirm."},"stakeLevel":{"type":"string","enum":["HIGH","MEDIUM","LOW"],"description":"Defaults MEDIUM. Not capped: a counterparty-confirmed outcome is stronger evidence than an auto-import, so HIGH is allowed here."},"transactionValue":{"type":"number","minimum":0},"dueDate":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"metadata":{"type":"object","additionalProperties":true,"description":"PII-free proposed-event metadata; merged with confirmation provenance stamps on write."},"counterpartyRef":{"type":"string","description":"Opaque id of the DISTINCT confirming counterparty. Must differ from userId."},"counterpartyName":{"type":"string","description":"Optional human label shown on the public preview."},"description":{"type":"string","description":"Optional human sentence shown to the counterparty (never the raw subject id)."},"returnUrl":{"type":"string","format":"uri","maxLength":2000,"description":"Optional. Where the HOSTED page (`confirmUrl`) returns the counterparty after they decide. Must be an absolute https URL on a public host with no embedded credentials; loopback/private/link-local destinations are refused (400). The redirect carries `credda_confirmation=confirmed|declined` and NOTHING else: never the token, the subject id, the counterparty ref, or a score."},"expiresInDays":{"type":"integer","minimum":1,"maximum":90,"description":"Confirmation window (default 14)."}}}}}},"responses":{"201":{"description":"Created (token shown once)","content":{"application/json":{"schema":{"type":"object","properties":{"confirmation":{"$ref":"#/components/schemas/ConfirmationRequest"},"confirmationToken":{"type":"string","description":"The one-time token, shown ONCE. Deliver it to the counterparty; only they should hold it."},"confirmUrl":{"type":"string","format":"uri","description":"The HOSTED \"Confirm with Credda\" page (GET /confirm/{id}?token=…). Send the counterparty here and you need no confirmation UI of your own: it is server-rendered, ships no JavaScript, works without an account, shows the same PII-free preview, and posts the decision to the same respond path. Credda never delivers this link; you do, over your own channel, so no address ever reaches the scoring service."},"previewUrl":{"type":"string","format":"uri","description":"Token-embedded URL a counterparty can GET to see what they are confirming. For platforms building their own UI instead of using confirmUrl."},"respondUrl":{"type":"string","format":"uri","description":"Where the counterparty POSTs their decision + token."}}}}}},"400":{"description":"Validation error, or counterpartyRef equals the subject (CONFIRMATION_SELF)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/confirmations/batch":{"post":{"tags":["Confirmations"],"summary":"Bulk-create confirmation requests (the activation engine)","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).","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional; makes a retried batch replay the original response instead of creating duplicates."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["requests"],"properties":{"requests":{"type":"array","minItems":1,"maxItems":100,"description":"Up to 100 proposals. Each has the same fields as POST /confirmations.","items":{"type":"object","required":["userId","eventType","counterpartyRef"],"properties":{"userId":{"type":"string","description":"The subject's externalId (the platform's own id)."},"eventType":{"type":"string","enum":["TRANSACTION_COMPLETED","CONTRACT_FULFILLED","REVIEW_VERIFIED","DISPUTE_RESOLVED_FOR_USER","TRANSACTION_CANCELLED","CONTRACT_CANCELLED","CONTRACT_BREACHED","TRANSACTION_DISPUTED"],"description":"The outcome to have confirmed; applied verbatim on confirm."},"stakeLevel":{"type":"string","enum":["HIGH","MEDIUM","LOW"],"description":"Defaults MEDIUM. Not capped: a counterparty-confirmed outcome is strong evidence."},"transactionValue":{"type":"number","minimum":0},"dueDate":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"metadata":{"type":"object","additionalProperties":true},"counterpartyRef":{"type":"string","description":"Opaque id of the DISTINCT confirming counterparty. Must differ from userId."},"counterpartyName":{"type":"string"},"description":{"type":"string"},"returnUrl":{"type":"string","format":"uri","maxLength":2000,"description":"Optional post-decision redirect for the hosted page. Strictly validated per item."},"expiresInDays":{"type":"integer","minimum":1,"maximum":90,"description":"Confirmation window (default 14)."}}}}}}}}},"responses":{"200":{"description":"Per-item results (partial success)","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"integer"},"created":{"type":"integer"},"failed":{"type":"integer"},"results":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","description":"Position in the submitted requests array."},"ok":{"type":"boolean"},"id":{"type":"string","description":"Confirmation request id (ok items)."},"userId":{"type":"string"},"status":{"type":"string","description":"PENDING for a created request."},"confirmationToken":{"type":"string","description":"One-time token, shown ONCE. Deliver it to the counterparty (ok items)."},"confirmUrl":{"type":"string","format":"uri","description":"The hosted \"Confirm with Credda\" page for this item (ok items)."},"error":{"type":"string","description":"Human reason (failed items)."},"code":{"type":"string","description":"Error code, e.g. CONFIRMATION_SELF (failed items)."}}}}}}}}},"400":{"description":"Validation error, or over the 100-item cap","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/confirmations/{id}":{"get":{"tags":["Confirmations"],"summary":"Fetch one of your confirmation requests","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Confirmation request","content":{"application/json":{"schema":{"type":"object","properties":{"confirmation":{"$ref":"#/components/schemas/ConfirmationRequest"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/confirmations/{id}/cancel":{"post":{"tags":["Confirmations"],"summary":"Cancel a still-pending request","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cancelled","content":{"application/json":{"schema":{"type":"object","properties":{"confirmation":{"$ref":"#/components/schemas/ConfirmationRequest"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already decided (CONFIRMATION_NOT_PENDING)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/confirmations/{id}/preview":{"get":{"tags":["Confirmations"],"summary":"What the counterparty is being asked to confirm (public, token-gated)","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.","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"token","in":"query","required":true,"schema":{"type":"string"},"description":"The one-time confirmation token."}],"responses":{"200":{"description":"Preview","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"Invalid/missing token (CONFIRMATION_TOKEN_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/confirmations/{id}/respond":{"post":{"tags":["Confirmations"],"summary":"Counterparty confirms or declines (public, token-gated)","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.","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token","decision"],"properties":{"token":{"type":"string","description":"The one-time confirmation token."},"decision":{"type":"string","enum":["confirm","decline"]}}}}}},"responses":{"200":{"description":"Decision applied","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["PENDING","CONFIRMED","DECLINED","EXPIRED","CANCELLED"]},"confirmation":{"$ref":"#/components/schemas/ConfirmationRequest"},"eventId":{"type":"string","description":"The ledger event written, present only on a confirm."}}}}}},"401":{"description":"Invalid token (CONFIRMATION_TOKEN_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already decided or expired (CONFIRMATION_NOT_PENDING)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/activation/campaigns":{"post":{"tags":["Activation"],"summary":"Onboard your whole book in one call (the activation engine)","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.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional; makes a retried submission replay the original response instead of creating a second campaign."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["rows"],"properties":{"name":{"type":"string","maxLength":200,"description":"Optional human label for the campaign (\"March 2026 roster import\"). Display only; never scored."},"rows":{"type":"array","minItems":1,"maxItems":500,"description":"Up to 500 roster rows. Each has the same fields as POST /confirmations, plus an optional `rowKey`.","items":{"type":"object","required":["userId","eventType","counterpartyRef"],"properties":{"userId":{"type":"string","description":"The subject's externalId (the platform's own id)."},"eventType":{"type":"string","enum":["TRANSACTION_COMPLETED","CONTRACT_FULFILLED","REVIEW_VERIFIED","DISPUTE_RESOLVED_FOR_USER","TRANSACTION_CANCELLED","CONTRACT_CANCELLED","CONTRACT_BREACHED","TRANSACTION_DISPUTED"],"description":"The outcome to have confirmed; applied verbatim on confirm."},"stakeLevel":{"type":"string","enum":["HIGH","MEDIUM","LOW"],"description":"Defaults MEDIUM. Not capped: a counterparty-confirmed outcome is strong evidence."},"transactionValue":{"type":"number","minimum":0},"dueDate":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"metadata":{"type":"object","additionalProperties":true},"counterpartyRef":{"type":"string","description":"Opaque id of the DISTINCT confirming counterparty. Must differ from userId."},"counterpartyName":{"type":"string"},"description":{"type":"string"},"returnUrl":{"type":"string","format":"uri","maxLength":2000,"description":"Optional post-decision redirect for the hosted page. Strictly validated per row."},"expiresInDays":{"type":"integer","minimum":1,"maximum":90,"description":"Confirmation window (default 14)."},"rowKey":{"type":"string","maxLength":255,"description":"Your own stable id for this roster row (e.g. a shift id). Makes the campaign idempotent per row."}}}}}}}}},"responses":{"201":{"description":"Campaign created (per-row tokens shown once)","content":{"application/json":{"schema":{"type":"object","properties":{"campaign":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","nullable":true},"submittedCount":{"type":"integer","description":"Rows accepted after in-batch row-key dedup."},"createdAt":{"type":"string","format":"date-time"}}},"created":{"type":"integer"},"failed":{"type":"integer"},"duplicates":{"type":"array","description":"Rows dropped as in-batch duplicates of an earlier row with the same rowKey.","items":{"type":"object","properties":{"index":{"type":"integer"},"rowKey":{"type":"string"}}}},"results":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","description":"Position in the submitted rows array (after dedup)."},"ok":{"type":"boolean"},"id":{"type":"string","description":"Confirmation request id (ok rows)."},"userId":{"type":"string"},"status":{"type":"string","description":"PENDING for a created request."},"rowKey":{"type":"string","description":"Echoed when the row carried one."},"confirmationToken":{"type":"string","description":"One-time token, shown ONCE. Deliver it to the counterparty (ok rows)."},"confirmUrl":{"type":"string","format":"uri","description":"The hosted \"Confirm with Credda\" page for this row (ok rows)."},"error":{"type":"string","description":"Human reason (failed rows)."},"code":{"type":"string","description":"Error code, e.g. CONFIRMATION_SELF (failed rows)."}}}}}}}}},"400":{"description":"Validation error, over the 500-row cap, or no row could be created (ACTIVATION_NO_VALID_ROWS)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/activation/campaigns/{id}":{"get":{"tags":["Activation"],"summary":"Campaign funnel (submitted → confirmed / declined / pending)","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Campaign + funnel","content":{"application/json":{"schema":{"type":"object","properties":{"campaign":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","nullable":true},"submittedCount":{"type":"integer"},"createdAt":{"type":"string","format":"date-time"}}},"funnel":{"type":"object","properties":{"submitted":{"type":"integer"},"pending":{"type":"integer"},"confirmed":{"type":"integer"},"declined":{"type":"integer"},"expired":{"type":"integer"},"cancelled":{"type":"integer"},"confirmationRate":{"type":"number","description":"confirmed / submitted (0 when nothing submitted). A factual completion ratio, never a score or judgement."}}}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/references":{"get":{"tags":["References"],"summary":"List your reference requests","parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["PENDING","CONFIRMED","DECLINED","EXPIRED","CANCELLED"]},"description":"Filter to one lifecycle state."}],"responses":{"200":{"description":"Reference requests","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ReferenceRequest"}},"nextCursor":{"type":"string","nullable":true,"description":"Pass as `cursor` to fetch the next page; `null` when there is no more data."}}}}}}}},"post":{"tags":["References"],"summary":"Propose a résumé claim for a reference to confirm","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.","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Optional; makes a retried create return the original request + token instead of a duplicate."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["userId","category","counterpartyRef"],"properties":{"userId":{"type":"string","description":"The subject's externalId (the platform's own id)."},"category":{"type":"string","enum":["education","skill","certification","employment"],"description":"The qualification category to have confirmed; recorded verbatim on confirm."},"label":{"type":"string","maxLength":200,"description":"Free-text claim label (e.g. \"Senior Engineer\", \"AWS Solutions Architect\"). Display only; never scored."},"issuer":{"type":"string","maxLength":200,"description":"Free-text employer/institution/issuer. Display only; never scored or ranked."},"jurisdiction":{"type":"string","maxLength":32,"description":"Where the credential applies (e.g. US-TX, GB). A property of the credential, never of the person."},"reference":{"type":"string","maxLength":100,"description":"The issuing body's own identifier for the credential. Display only; Credda does not resolve it."},"counterpartyRef":{"type":"string","description":"Opaque id of the DISTINCT confirming third party (past employer/manager/client/colleague/issuing body). Must differ from userId."},"counterpartyName":{"type":"string","description":"Optional human label shown on the public preview."},"description":{"type":"string","description":"Optional human sentence shown to the counterparty (never the raw subject id)."},"returnUrl":{"type":"string","format":"uri","maxLength":2000,"description":"Optional. Where the HOSTED page (`referenceUrl`) returns the counterparty after they decide. Must be an absolute https URL on a public host with no embedded credentials; loopback/private/link-local destinations are refused (400). The redirect carries `credda_confirmation=confirmed|declined` and NOTHING else."},"expiresInDays":{"type":"integer","minimum":1,"maximum":90,"description":"Confirmation window (default 14)."}}}}}},"responses":{"201":{"description":"Created (token shown once)","content":{"application/json":{"schema":{"type":"object","properties":{"reference":{"$ref":"#/components/schemas/ReferenceRequest"},"referenceToken":{"type":"string","description":"The one-time token, shown ONCE. Deliver it to the counterparty; only they should hold it."},"referenceUrl":{"type":"string","format":"uri","description":"The HOSTED \"Confirm a reference with Credda\" page (GET /reference/{id}?token=…). Send the counterparty here and you need no UI of your own: server-rendered, ships no JavaScript, works without an account, shows the same PII-free preview, and posts the decision to the same respond path. Credda never delivers this link; you do."},"previewUrl":{"type":"string","format":"uri","description":"Token-embedded URL a counterparty can GET to see what they are confirming. For platforms building their own UI instead of using referenceUrl."},"respondUrl":{"type":"string","format":"uri","description":"Where the counterparty POSTs their decision + token."}}}}}},"400":{"description":"Validation error, or counterpartyRef equals the subject (REFERENCE_SELF)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/references/{id}":{"get":{"tags":["References"],"summary":"Fetch one of your reference requests","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Reference request","content":{"application/json":{"schema":{"type":"object","properties":{"reference":{"$ref":"#/components/schemas/ReferenceRequest"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/references/{id}/cancel":{"post":{"tags":["References"],"summary":"Cancel a still-pending request","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cancelled","content":{"application/json":{"schema":{"type":"object","properties":{"reference":{"$ref":"#/components/schemas/ReferenceRequest"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already decided (REFERENCE_NOT_PENDING)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/references/{id}/preview":{"get":{"tags":["References"],"summary":"What the counterparty is being asked to confirm (public, token-gated)","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.","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"token","in":"query","required":true,"schema":{"type":"string"},"description":"The one-time reference token."}],"responses":{"200":{"description":"Preview","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"description":"Invalid/missing token (REFERENCE_TOKEN_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/references/{id}/respond":{"post":{"tags":["References"],"summary":"Counterparty confirms or declines (public, token-gated)","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.","security":[],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["token","decision"],"properties":{"token":{"type":"string","description":"The one-time reference token."},"decision":{"type":"string","enum":["confirm","decline"]}}}}}},"responses":{"200":{"description":"Decision applied","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["PENDING","CONFIRMED","DECLINED","EXPIRED","CANCELLED"]},"reference":{"$ref":"#/components/schemas/ReferenceRequest"},"eventId":{"type":"string","description":"The qualification event recorded, present only on a confirm."}}}}}},"401":{"description":"Invalid token (REFERENCE_TOKEN_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already decided or expired (REFERENCE_NOT_PENDING)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/screenings":{"get":{"tags":["Screenings"],"summary":"List your screening jobs (status + summary only)","parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Screenings","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ScreeningJob"}},"nextCursor":{"type":"string","nullable":true,"description":"Pass as `cursor` to fetch the next page; `null` when there is no more data."}}}}}}}},"post":{"tags":["Screenings"],"summary":"Submit an async bulk screening (up to 10,000 ids)","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.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["userIds"],"properties":{"userIds":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":10000,"description":"External user ids to screen (deduped server-side)."}}}}}},"responses":{"202":{"description":"Accepted (inline jobs return already COMPLETED)","content":{"application/json":{"schema":{"type":"object","properties":{"screening":{"$ref":"#/components/schemas/ScreeningJob"}}}}}},"400":{"description":"No ids / too many ids","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Queue unavailable for a large job (SCREENING_QUEUE_UNAVAILABLE)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/screenings/{id}":{"get":{"tags":["Screenings"],"summary":"Screening status + summary (no results payload)","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Screening","content":{"application/json":{"schema":{"type":"object","properties":{"screening":{"$ref":"#/components/schemas/ScreeningJob"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/screenings/{id}/results":{"get":{"tags":["Screenings"],"summary":"Full per-user results (JSON, or ?format=csv attachment)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["json","csv"]},"description":"'csv' returns a text/csv attachment with a header row and one row per screened user."}],"responses":{"200":{"description":"Results","content":{"application/json":{"schema":{"type":"object","properties":{"screening":{"$ref":"#/components/schemas/ScreeningJob"},"results":{"type":"array","items":{"$ref":"#/components/schemas/ScreeningResultItem"}},"count":{"type":"integer"}}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Job not COMPLETED yet, or FAILED (SCREENING_NOT_COMPLETED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/verify/{token}":{"get":{"tags":["Verify"],"summary":"Resolve a share token to a PII-free payload + credential","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.","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}},{"name":"scope","in":"query","required":false,"schema":{"type":"string","enum":["full","band","minimal"]}},{"name":"earnings","in":"query","required":false,"description":"Set to 1 to attach a Verified Earnings summary (scope=full only).","schema":{"type":"string","enum":["1"]}},{"name":"professional","in":"query","required":false,"description":"Set to 1 to attach the Professional Record summary (scope=full only).","schema":{"type":"string","enum":["1"]}},{"name":"profile","in":"query","required":false,"description":"Set to 1 to attach the consolidated professional profile: reliability + verified qualifications + linked projects (scope=full only).","schema":{"type":"string","enum":["1"]}},{"name":"benchmark","in":"query","required":false,"description":"Set to 1 to attach a COARSE population comparison (scope=full only): a quartile-grain label (`top_decile` / `top_quartile` / `above_median` / `below_median`) against the whole scored population, never a precise percentile and never the population size. Absent (`null`) unless the population clears the k-anonymity floor. A distribution fact, not a verdict.","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"Verification payload","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"Unknown/rotated token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/verify/presentation":{"post":{"tags":["Wallets"],"summary":"Verify an SD-JWT VC presentation a holder handed you","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.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"vp_token":{"type":"string","description":"The SD-JWT VC presentation (`<JWT>~<disclosure>~…~`)."},"presentation":{"type":"string","description":"Alias for `vp_token`."}}}}}},"responses":{"200":{"description":"Verified presentation","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean","enum":[true]},"issuer":{"type":"string"},"vct":{"type":"string"},"claims":{"type":"object","additionalProperties":true},"disclosedClaims":{"type":"array","items":{"type":"string"}},"issuedAt":{"type":"string","format":"date-time","nullable":true},"expiresAt":{"type":"string","format":"date-time","nullable":true},"credentialStatus":{"type":"object","additionalProperties":true,"nullable":true},"statusList":{"type":"string"},"keyBound":{"type":"boolean"}}}}}},"400":{"description":"Not a valid Credda SD-JWT VC presentation (bad signature, expired, or a disclosure that does not match `_sd`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/verify/{token}/credential":{"get":{"tags":["Verify"],"summary":"Signed credential (?format=w3c → W3C VC-JWT)","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["credda","w3c"]}}],"responses":{"200":{"description":"Credential","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/v1/verify/{token}/delivery-receipts":{"get":{"tags":["Agents"],"summary":"Delivery receipts + a signed delivery credential","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.","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}},{"name":"ttl","in":"query","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Delivery receipts + credential","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"Test-mode subject","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown/rotated token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/agents":{"post":{"tags":["Agents"],"summary":"Register (or update) an agent subject","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).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["userId"],"properties":{"userId":{"type":"string","description":"The agent's external id on your platform"},"operatedByReportingPlatform":{"type":"boolean","default":true,"description":"Do YOU operate this agent? Default true (the conservative reading)."},"operator":{"type":"object","properties":{"name":{"type":"string"},"homepage":{"type":"string"},"did":{"type":"string","description":"e.g. did:web:acme.ai"}}},"modelFamily":{"type":"string","description":"Free text, never validated (e.g. \"claude-opus-4-8\")."},"description":{"type":"string"}}}}}},"responses":{"201":{"description":"Agent subject","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Third-party operator declared but not named (OPERATOR_NOT_DECLARED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Subject already has a person record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/agents/{id}":{"get":{"tags":["Agents"],"summary":"Inspect an agent subject","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Agent subject","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"Not an agent / not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/businesses":{"post":{"tags":["Businesses"],"summary":"Register (or update) a business subject","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.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["userId","business"],"properties":{"userId":{"type":"string","description":"The business's external id on your platform"},"controlledByReportingPlatform":{"type":"boolean","default":true,"description":"Do YOU control this business record? Default true (the conservative reading)."},"business":{"type":"object","description":"Declared business facts. Claims, never evidence, and never a scoring input; industry is never ranked.","properties":{"legalName":{"type":"string"},"tradingName":{"type":"string"},"homepage":{"type":"string"},"did":{"type":"string","description":"e.g. did:web:mapledental.example"},"industry":{"type":"string","description":"Free text (\"dental practice\", \"electrical contractor\"). Never validated, never ranked."},"jurisdiction":{"type":"string"},"registrationId":{"type":"string","description":"Declared company/registration number. Never verified by Credda, and never presented as verified."}}},"description":{"type":"string"}}}}}},"responses":{"201":{"description":"Business subject","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"No business identity declared (BUSINESS_IDENTITY_NOT_DECLARED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Subject already has a person/agent record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/businesses/{id}":{"get":{"tags":["Businesses"],"summary":"Inspect a business subject","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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Business subject","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"Not a business / not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/businesses/{id}/record":{"get":{"tags":["Businesses"],"summary":"The counterparty-confirmed B2B record on its own","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).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Business record","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"Not a business / not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/verify/{token}/business-record":{"get":{"tags":["Businesses"],"summary":"Business record + a signed business credential","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.","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}},{"name":"ttl","in":"query","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Business record + credential","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"403":{"description":"Test-mode subject","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown/rotated token, or not a business","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/verify/{token}/export":{"get":{"tags":["Verify"],"summary":"Portable, self-verifying trust export","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.","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Trust export bundle","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"Unknown/rotated token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/verify/{token}/career-export":{"get":{"tags":["Verify"],"summary":"Portable professional record in an open résumé schema","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.","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["jsonresume","jsonld"]}}],"responses":{"200":{"description":"JSON Resume document (or schema.org JSON-LD when format=jsonld)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Unsupported format (CAREER_FORMAT_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown/rotated token or no record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/verify/{token}/reliability-report":{"get":{"tags":["Verify"],"summary":"Worker-consent reliability report","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.","security":[],"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string"}},{"name":"recent","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50,"default":10}},{"name":"benchmark","in":"query","required":false,"schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"Reliability report (or reliabilityReport: null)","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"},"issuer":{"type":"string"},"reliabilityReport":{"type":"object","nullable":true,"additionalProperties":true}}}}}},"404":{"description":"Unknown/rotated token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/status/revocation":{"get":{"tags":["Verify"],"summary":"StatusList2021 revocation list (signed VC)","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.","security":[],"responses":{"200":{"description":"StatusList2021 Verifiable Credential","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/v1/scoring/model":{"get":{"tags":["Scores"],"summary":"The scoring model, as data","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.","security":[],"responses":{"200":{"description":"Scoring model","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/v1/webhooks/events":{"get":{"tags":["Webhooks"],"summary":"Outbound webhook event catalog","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.","security":[],"responses":{"200":{"description":"Webhook event catalog","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/v1/plans":{"get":{"tags":["Plans"],"summary":"Developer plan catalog","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.","security":[],"responses":{"200":{"description":"Plan catalog","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/v1/errors":{"get":{"tags":["Reference"],"summary":"Machine-readable error catalog","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.","security":[],"responses":{"200":{"description":"Error catalog","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/v1/enums":{"get":{"tags":["Reference"],"summary":"Self-describing enums","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.","security":[],"responses":{"200":{"description":"Enum catalog","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/v1/outcome-templates":{"get":{"tags":["Reference"],"summary":"Industry outcome templates: map real-world work to events","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.","security":[],"parameters":[{"name":"industry","in":"query","required":false,"description":"Filter to one industry slug (e.g. `dental_clinical`, `trades_home_services`).","schema":{"type":"string"}}],"responses":{"200":{"description":"Outcome template catalog","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/v1/changelog":{"get":{"tags":["Reference"],"summary":"API version contract, changelog and deprecation policy","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.","security":[],"responses":{"200":{"description":"Version contract + changelog","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/v1/reason-codes":{"get":{"tags":["Reference"],"summary":"Adverse-action reason-code catalog","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.","security":[],"responses":{"200":{"description":"Reason-code catalog","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/v1/benchmarks":{"get":{"tags":["Benchmarks"],"summary":"Benchmark catalog (cohort dimensions + k-anonymity floor)","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.","security":[],"responses":{"200":{"description":"Benchmark catalog","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/v1/benchmarks/distribution":{"get":{"tags":["Benchmarks"],"summary":"Aggregate score distribution for a cohort","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.","parameters":[{"name":"dimension","in":"query","required":false,"schema":{"type":"string","enum":["all","subjectType","verificationDepthBand","activityVolumeBand","tenureBand"]},"description":"Cohort dimension. Default `all`."},{"name":"cohort","in":"query","required":false,"schema":{"type":"string"},"description":"A single cohort value on the dimension. Omit for all values."}],"responses":{"200":{"description":"Cohort distribution(s); some may be `available:false` under the k-anonymity floor","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Unknown cohort dimension","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/widget/credda-widget.js":{"get":{"tags":["Widget"],"summary":"Embeddable trust-badge script","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.","security":[],"responses":{"200":{"description":"JavaScript source","content":{"application/javascript":{"schema":{"type":"string"}}}}}}},"/verify-embed.js":{"get":{"tags":["Widget"],"summary":"Embeddable \"Confirm with Credda\" launcher","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.","security":[],"responses":{"200":{"description":"JavaScript source","content":{"application/javascript":{"schema":{"type":"string"}}}}}}},"/verify-embed-bridge.js":{"get":{"tags":["Widget"],"summary":"Completion bridge for the embedded confirmation flow","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.","security":[],"responses":{"200":{"description":"JavaScript source","content":{"application/javascript":{"schema":{"type":"string"}}}}}}},"/.well-known/jwks.json":{"get":{"tags":["Well-Known"],"summary":"Verifying keys (Ed25519/OKP)","security":[],"responses":{"200":{"description":"JWKS","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/.well-known/did.json":{"get":{"tags":["Well-Known"],"summary":"did:web issuer DID document","security":[],"responses":{"200":{"description":"DID document","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/.well-known/credda-trust-registry.json":{"get":{"tags":["Well-Known"],"summary":"Recognized issuers","security":[],"responses":{"200":{"description":"Trust registry","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/.well-known/http-message-signatures-directory":{"get":{"tags":["Well-Known"],"summary":"Web Bot Auth signature directory (Ed25519 JWKS)","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.","security":[],"responses":{"200":{"description":"Signature directory (JWKS)","content":{"application/http-message-signatures-directory+json":{"schema":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"object","properties":{"kty":{"type":"string","const":"OKP"},"crv":{"type":"string","const":"Ed25519"},"x":{"type":"string","description":"base64url public key"},"kid":{"type":"string","description":"base64url RFC 7638 JWK thumbprint, the `keyid` signature parameter"},"use":{"type":"string","const":"sig"},"nbf":{"type":"integer"},"exp":{"type":"integer"}}}},"x-credda-spec-versions":{"type":"object","additionalProperties":true,"description":"The exact draft revisions implemented, including a recorded divergence on the `Signature-Agent` header form."}},"additionalProperties":true}}}}}}},"/.well-known/openid-credential-issuer":{"get":{"tags":["Wallets"],"summary":"OID4VCI Credential Issuer Metadata","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.","security":[],"responses":{"200":{"description":"Credential Issuer Metadata","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/.well-known/oauth-authorization-server":{"get":{"tags":["Wallets"],"summary":"OAuth 2.0 Authorization Server Metadata (RFC 8414)","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.","security":[],"responses":{"200":{"description":"Authorization Server Metadata","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/oid4vci/token":{"post":{"tags":["Wallets"],"summary":"Exchange a pre-authorized code for an access token","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.","security":[],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["grant_type","pre-authorized_code"],"properties":{"grant_type":{"type":"string","enum":["urn:ietf:params:oauth:grant-type:pre-authorized_code"]},"pre-authorized_code":{"type":"string"}}}}}},"responses":{"200":{"description":"Access token","content":{"application/json":{"schema":{"type":"object","properties":{"access_token":{"type":"string"},"token_type":{"type":"string","enum":["Bearer"]},"expires_in":{"type":"integer"}}}}}},"400":{"description":"OAuth error (invalid_request / invalid_grant / unsupported_grant_type)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"503":{"description":"Issuance store unavailable, retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/oid4vci/nonce":{"post":{"tags":["Wallets"],"summary":"Get a fresh c_nonce for a key proof","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.","security":[],"responses":{"200":{"description":"Nonce","content":{"application/json":{"schema":{"type":"object","properties":{"c_nonce":{"type":"string"}}}}}},"503":{"description":"Issuance store unavailable, retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/oid4vci/credential":{"post":{"tags":["Wallets"],"summary":"Issue the credential into the wallet","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.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"credential_configuration_id":{"type":"string","description":"Required unless the offer named exactly one configuration."},"proofs":{"type":"object","properties":{"jwt":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":1}}}}}}}},"responses":{"200":{"description":"Credential","content":{"application/json":{"schema":{"type":"object","properties":{"credentials":{"type":"array","items":{"type":"object","properties":{"credential":{"type":"string"}}}},"x-credda-disclosable-claims":{"type":"array","items":{"type":"string"},"description":"Claim names the holder may withhold at presentation time (SD-JWT only; empty for jwt_vc_json)."},"x-credda-disclosure-scope":{"type":"string","enum":["minimal","band","full"]}}}}}},"400":{"description":"OID4VCI credential error (invalid_proof / invalid_nonce / unknown_credential_configuration / credential_request_denied)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"invalid_token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"503":{"description":"Issuance store unavailable, retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/api/v1/test/seed":{"post":{"tags":["Test Mode"],"summary":"Seed the sandbox with synthetic subjects (test keys only)","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.","responses":{"201":{"description":"Seed summary, with each subject's freshly computed score","content":{"application/json":{"schema":{"type":"object","properties":{"seeded":{"type":"boolean"},"livemode":{"type":"boolean","description":"Always false; this is sandbox data."},"seedVersion":{"type":"integer"},"subjectsCreated":{"type":"integer"},"subjectsSkipped":{"type":"integer","description":"Subjects that already held events."},"eventsWritten":{"type":"integer"},"subjects":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string","example":"sbx_reliable_courier"},"label":{"type":"string"},"record":{"type":"string","description":"What the record looks like, never a promised band."},"tryNext":{"type":"string"},"totalEvents":{"type":"integer"},"eventsWritten":{"type":"integer"},"alreadySeeded":{"type":"boolean"},"finalScore":{"type":"number","nullable":true},"scoreBand":{"type":"string","nullable":true},"confidence":{"type":"number","nullable":true}}}},"note":{"type":"string"},"nextSteps":{"type":"array","items":{"type":"string"}}}}}}},"403":{"description":"TEST_MODE_ONLY, seeding requires a crd_test_ key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/test/data":{"delete":{"tags":["Test Mode"],"summary":"Reset the platform's test data (test keys only)","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.","responses":{"200":{"description":"Reset summary","content":{"application/json":{"schema":{"type":"object","properties":{"reset":{"type":"boolean"},"deleted":{"type":"object","properties":{"users":{"type":"integer"},"events":{"type":"integer"},"screenings":{"type":"integer"}}},"note":{"type":"string"}}}}}},"403":{"description":"TEST_MODE_ONLY, a live key can never delete anything","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/open-badges/achievements":{"get":{"tags":["Open Badges"],"summary":"The closed set of Open Badges 3.0 achievements this issuer will sign","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.","security":[],"responses":{"200":{"description":"Achievement definitions","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/v1/open-badges/achievements/{badgeId}":{"get":{"tags":["Open Badges"],"summary":"One achievement definition","security":[],"parameters":[{"name":"badgeId","in":"path","required":true,"schema":{"type":"string","enum":["early-adopter","first-delivery","ten-strong","century-club","verified-professional","multi-platform","flawless-ten","one-year-reliable","identity-verified"]}}],"responses":{"200":{"description":"Achievement","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"Unknown achievement","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/admin/open-badges":{"post":{"tags":["Open Badges"],"summary":"Sign Open Badges 3.0 credentials for an anchored subject","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.","security":[{"adminSecret":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["shareToken","awards"],"properties":{"shareToken":{"type":"string","description":"The subject's existing Credda share token, the public trust anchor that owns the revocation slot."},"awards":{"type":"array","minItems":1,"maxItems":25,"items":{"type":"object","required":["badgeId","earnedAt"],"properties":{"badgeId":{"type":"string","enum":["early-adopter","first-delivery","ten-strong","century-club","verified-professional","multi-platform","flawless-ten","one-year-reliable","identity-verified"]},"earnedAt":{"type":"string","format":"date-time","description":"When the criteria were first met. Becomes `validFrom`. Must not be in the future."}}}}}}}}},"responses":{"201":{"description":"Signed Open Badges collection","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Unknown achievement id, or an invalid/future earnedAt","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Test-mode subject","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown or revoked trust token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/admin/platforms":{"get":{"tags":["Admin"],"summary":"List platforms (+ optional consolidated usage)","security":[{"adminSecret":[]}],"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.","parameters":[{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"referredBy","in":"query","required":false,"schema":{"type":"string"},"description":"Filter to platforms provisioned by this parent platform id."},{"name":"usage","in":"query","required":false,"schema":{"type":"string","enum":["1","true"]},"description":"Attach per-platform usage summaries + an aggregate over the page."},{"name":"days","in":"query","required":false,"schema":{"type":"integer","minimum":1},"description":"Usage window (trailing days). Only read when `usage` is set."}],"responses":{"200":{"description":"Platforms","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":true}},"nextCursor":{"type":["string","null"]},"window":{"type":"object","additionalProperties":true,"description":"Present only when `usage` is set."},"aggregateUsage":{"type":"object","additionalProperties":true,"description":"Consolidated totals across the returned page; present only when `usage` is set."}}}}}}}},"post":{"tags":["Admin"],"summary":"Create a platform","security":[{"adminSecret":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","trustTier"],"properties":{"name":{"type":"string"},"trustTier":{"type":"string","enum":["ENTERPRISE","GROWTH","STARTER","SELF_REPORTED"]},"referredByPlatformId":{"type":"string","description":"Partner attribution (reseller/ISV model, docs/PARTNER_PLATFORM.md): the parent platform provisioning this one. Optional + additive; pure provenance, grants the parent no access and never touches a score. Must reference an existing platform."}}}}}},"responses":{"201":{"description":"Created"},"400":{"description":"referredByPlatformId does not reference an existing platform","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/admin/platforms/{id}/keys":{"get":{"tags":["Admin"],"summary":"List a platform's API keys (metadata only)","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.","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"API keys","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"404":{"description":"Platform not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Admin"],"summary":"Generate an API key (shown once)","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.","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["label"],"properties":{"label":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"expiresInDays":{"type":"integer","minimum":1,"maximum":3650},"isTest":{"type":"boolean","default":false,"description":"Issue a sandbox (crd_test_) key. Free at every tier; its writes and reads are confined to the platform's private test universe."}}}}}},"responses":{"201":{"description":"API key"}}}},"/api/v1/admin/platforms/{id}/keys/{keyId}":{"patch":{"tags":["Admin"],"summary":"Re-scope an already-issued API key in place","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.","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"keyId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["scopes"],"properties":{"scopes":{"type":"array","minItems":1,"items":{"type":"string"},"description":"The key's new scope set, replacing the old one entirely. Coarse (\"read\"/\"write\"/\"*\") or per-resource (\"events:write\", \"scores:read\", \"webhooks:*\", …)."}}}}}},"responses":{"200":{"description":"Updated key metadata (never a hash, never plaintext)","content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"object","additionalProperties":true}}}}}},"400":{"description":"Missing or invalid scopes (an empty array is refused)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Platform or key not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/admin/platforms/{id}/keys/{keyId}/rotate":{"post":{"tags":["Admin"],"summary":"Rotate an API key with a grace overlap (new key shown once)","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.","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"keyId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"graceHours":{"type":"number","minimum":0,"maximum":168,"default":24,"description":"How long the old key keeps working after rotation."}}}}}},"responses":{"201":{"description":"New key (plaintext shown once) + old-key grace deadline","content":{"application/json":{"schema":{"type":"object","properties":{"apiKey":{"type":"string"},"id":{"type":"string"},"label":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"expiresAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"rotatedFrom":{"type":"object","properties":{"id":{"type":"string"},"expiresAt":{"type":"string","format":"date-time","description":"When the old key stops authenticating."}}}}}}}},"400":{"description":"Key already revoked or expired","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Platform or key not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/admin/platforms/{id}":{"patch":{"tags":["Admin"],"summary":"Update trust tier / deactivate / velocity-exempt","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"trustTier":{"type":"string","enum":["ENTERPRISE","GROWTH","STARTER","SELF_REPORTED"]},"isActive":{"type":"boolean"},"velocityExempt":{"type":"boolean","description":"Exempt a TRUSTED first-party reporter from the per-user anti-Sybil velocity cap. Only the Credda product backend, never a third party."}}}}}},"responses":{"200":{"description":"Updated"}}}},"/api/v1/admin/users/{id}/resolve-velocity-flag":{"post":{"tags":["Admin"],"summary":"Clear a velocity flag and recompute","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cleared"}}}},"/api/v1/admin/users/{id}/audit-log":{"get":{"tags":["Admin"],"summary":"Paginated audit trail","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Audit log"}}}},"/api/v1/admin/users/{id}/share-token":{"delete":{"tags":["Admin"],"summary":"Withdraw a subject's public trust surface (admin mirror)","security":[{"adminSecret":[]}],"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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revocation outcome","content":{"application/json":{"schema":{"type":"object","properties":{"externalId":{"type":"string"},"revoked":{"type":"boolean","description":"True when a live share token was present and has now been withdrawn."},"alreadyRevoked":{"type":"boolean","description":"True when there was nothing to revoke; the call is still a success."},"statusListIndex":{"type":["integer","null"],"description":"The subject's StatusList2021 slot; null only when they never minted a token and so never had a credential."},"statusListCredential":{"type":"string","format":"uri"},"note":{"type":"string","description":"Plain-English statement of what revocation does and does not achieve."}}}}}},"404":{"description":"Unknown externalId (USER_NOT_FOUND)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/admin/platforms/{id}/webhooks":{"get":{"tags":["Admin"],"summary":"List a platform's webhooks","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Webhooks","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}}}}}}},"404":{"description":"Platform not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Admin"],"summary":"Subscribe an HTTPS endpoint on behalf of a platform (secret returned once)","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","minItems":1,"items":{"type":"string","enum":["score.updated","score.band_changed","dispute.resolved","monitor.triggered","policy.threshold_crossed","usage.quota_warning","import.completed","screening.completed","confirmation.awaiting_response","confirmation.expiring_soon"]}},"description":{"type":"string"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"webhook":{"$ref":"#/components/schemas/Webhook"},"secret":{"type":"string","description":"Signing secret, shown ONCE."}}}}}},"400":{"description":"Invalid url/events","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Platform not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/admin/platforms/{id}/webhooks/{webhookId}":{"patch":{"tags":["Admin"],"summary":"Update url/events/description/isActive","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"webhookId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"type":"object","properties":{"webhook":{"$ref":"#/components/schemas/Webhook"}}}}}},"404":{"description":"Platform or webhook not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Admin"],"summary":"Delete a webhook","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"webhookId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"Platform or webhook not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/admin/platforms/{id}/webhooks/{webhookId}/test":{"post":{"tags":["Admin"],"summary":"Send a synthetic signed delivery","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"webhookId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Delivery result","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"Platform or webhook not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/admin/platforms/{id}/webhooks/{webhookId}/deliveries":{"get":{"tags":["Admin"],"summary":"Recent delivery attempts","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"webhookId","in":"path","required":true,"schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}}],"responses":{"200":{"description":"Deliveries","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","additionalProperties":true}},"nextCursor":{"type":"string","nullable":true,"description":"Pass as `cursor` to fetch the next page; `null` when there is no more data."}}}}}},"404":{"description":"Platform or webhook not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/admin/platforms/{id}/webhooks/{webhookId}/deliveries/{deliveryId}/replay":{"post":{"tags":["Admin"],"summary":"Replay a past delivery (admin mirror)","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"webhookId","in":"path","required":true,"schema":{"type":"string"}},{"name":"deliveryId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Replay outcome","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Payload not retained","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/admin/platforms/{id}/monitors":{"get":{"tags":["Admin"],"summary":"List a platform's live score monitors","security":[{"adminSecret":[]}],"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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Monitors","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Monitor"}},"nextCursor":{"type":"string","nullable":true,"description":"Pass as `cursor` to fetch the next page; `null` when there is no more data."}}}}}},"404":{"description":"Platform not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Admin"],"summary":"Create a score monitor on behalf of a platform","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"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}`).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["userId"],"properties":{"userId":{"type":"string","description":"The platform's external user id (externalId)."},"belowScore":{"type":"number","minimum":0,"maximum":100},"aboveScore":{"type":"number","minimum":0,"maximum":100},"onBandChange":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"monitor":{"$ref":"#/components/schemas/Monitor"}}}}}},"400":{"description":"No condition provided / validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Platform not found, or unknown userId (USER_NOT_FOUND)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Active monitor limit reached for the plan tier (MONITOR_LIMIT_REACHED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/admin/platforms/{id}/monitors/{monitorId}":{"get":{"tags":["Admin"],"summary":"Fetch one monitor (admin mirror)","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"monitorId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Monitor","content":{"application/json":{"schema":{"type":"object","properties":{"monitor":{"$ref":"#/components/schemas/Monitor"}}}}}},"404":{"description":"Platform or monitor not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"tags":["Admin"],"summary":"Update thresholds / onBandChange / isActive (admin mirror)","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"monitorId","in":"path","required":true,"schema":{"type":"string"}}],"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.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"belowScore":{"type":"number","nullable":true,"minimum":0,"maximum":100},"aboveScore":{"type":"number","nullable":true,"minimum":0,"maximum":100},"onBandChange":{"type":"boolean"},"isActive":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"type":"object","properties":{"monitor":{"$ref":"#/components/schemas/Monitor"}}}}}},"400":{"description":"Update would leave the monitor with no condition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Platform or monitor not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Active monitor limit reached (MONITOR_LIMIT_REACHED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Admin"],"summary":"Delete a monitor (admin mirror)","security":[{"adminSecret":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"monitorId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"Platform or monitor not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/admin/platforms/{id}/usage":{"get":{"tags":["Admin"],"summary":"A platform's usage vs. tier quota, on behalf of it","security":[{"adminSecret":[]}],"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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"days","in":"query","required":false,"description":"Trailing window in days (default 7, max 400). Mutually exclusive with `from`/`to`.","schema":{"type":"integer","minimum":1,"maximum":400}},{"name":"from","in":"query","required":false,"description":"Inclusive range start (ISO date, YYYY-MM-DD). Requires `to`; clamped to the 400-day history window (completed days beyond the 90-day live retention are served from durable daily rollups).","schema":{"type":"string","format":"date"}},{"name":"to","in":"query","required":false,"description":"Inclusive range end (ISO date, YYYY-MM-DD). Requires `from`.","schema":{"type":"string","format":"date"}},{"name":"format","in":"query","required":false,"description":"Response format: 'json' (default) or 'csv' (flat statement, `Content-Disposition: attachment`).","schema":{"type":"string","enum":["json","csv"]}}],"responses":{"200":{"description":"Usage summary (JSON by default; CSV statement when format=csv)","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}},"text/csv":{"schema":{"type":"string"}}}},"400":{"description":"Invalid window (USAGE_WINDOW_CONFLICT / USAGE_RANGE_INVALID / USAGE_FORMAT_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Platform not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/admin/platforms/{id}/activity":{"get":{"tags":["Admin"],"summary":"A platform's own activity log, on behalf of it","security":[{"adminSecret":[]}],"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.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"description":"Opaque cursor: the `nextCursor` from a previous page. Omit for the first page.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Page size (1–100, default 25).","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"action","in":"query","required":false,"description":"Filter to one audit action.","schema":{"type":"string","enum":["EVENT_CREATED","VELOCITY_FLAG_SET","PLATFORM_CREATED","PLATFORM_UPDATED","API_KEY_GENERATED","API_KEY_ROTATED","API_KEY_REVOKED","SHARE_TOKEN_MINTED","SHARE_TOKEN_REVOKED","CREDENTIAL_OFFER_CREATED","WEBHOOK_CREATED","WEBHOOK_UPDATED","WEBHOOK_DELETED","MONITOR_CREATED","MONITOR_UPDATED","MONITOR_DELETED","NOTIFICATION_CHANNEL_CREATED","NOTIFICATION_CHANNEL_DELETED","CONFIRMATION_REQUEST_CREATED","CONFIRMATION_REQUEST_CONFIRMED","CONFIRMATION_REQUEST_DECLINED","CONFIRMATION_REQUEST_CANCELLED","TEST_DATA_RESET","TEST_DATA_SEEDED"]}},{"name":"from","in":"query","required":false,"description":"Inclusive lower bound on `createdAt` (ISO timestamp or YYYY-MM-DD).","schema":{"type":"string"}},{"name":"to","in":"query","required":false,"description":"Inclusive upper bound on `createdAt` (ISO timestamp or YYYY-MM-DD).","schema":{"type":"string"}}],"responses":{"200":{"description":"Activity page","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ActivityEntry"}},"nextCursor":{"type":"string","nullable":true,"description":"Pass as `cursor` to fetch the next page; `null` when there is no more data."}}}}}},"400":{"description":"Invalid query (ACTIVITY_ACTION_INVALID / ACTIVITY_RANGE_INVALID)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Platform not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"platformKey":{"type":"http","scheme":"bearer","description":"Per-platform API key: live (`crd_live_…`) or sandbox (`crd_test_…`; confined to the platform's private test universe, see the Test mode section). Carries scopes: coarse `read` (GET/HEAD) / `write` (mutations) / `*`, or fine-grained `<resource>:<action>` (e.g. `events:write`, `scores:read`, `webhooks:*`, and `admin:*` for programmatic self-administration under /api/v1/platform). Subject to per-minute rate limits and optional monthly quotas by trust tier (test requests never count toward quota)."},"adminSecret":{"type":"apiKey","in":"header","name":"X-Admin-Secret","description":"Operator-only secret."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string","description":"Human-readable message"},"code":{"type":"string","enum":["ACTIVATION_NO_VALID_ROWS","ACTIVITY_ACTION_INVALID","ACTIVITY_FORMAT_INVALID","ACTIVITY_RANGE_INVALID","ANALYTICS_RANGE_INVALID","ANALYTICS_WINDOW_CONFLICT","BAD_REQUEST","BOOK_FORMAT_INVALID","BOOK_QUERY_INVALID","BUSINESS_IDENTITY_NOT_DECLARED","CAREER_FORMAT_INVALID","CONFIRMATION_NOT_PENDING","CONFIRMATION_SELF","CONFIRMATION_TOKEN_INVALID","CONFLICT","DISPATCH_CONTEXT_INVALID","EXPORT_FORMAT_INVALID","EXPORT_RANGE_INVALID","FORBIDDEN","IDEMPOTENCY_KEY_REUSED","IMPORT_CSV_INVALID","IMPORT_QUEUE_UNAVAILABLE","IMPORT_TOO_MANY_ROWS","INGEST_MAPPING_INVALID","INTERNAL_ERROR","MONITOR_LIMIT_REACHED","NOTIFICATION_CHANNEL_INVALID","NOTIFICATION_CHANNEL_LIMIT_REACHED","NOT_FOUND","OPERATOR_NOT_DECLARED","PAYLOAD_NOT_RETAINED","PLAN_REQUIRED","POLICY_LIMIT_REACHED","QUOTA_EXCEEDED","RATE_LIMIT_EXCEEDED","REFERENCE_NOT_PENDING","REFERENCE_SELF","REFERENCE_TOKEN_INVALID","RESERVED_EXTERNAL_ID","SCOPE_CEILING_EXCEEDED","SCOPE_INSUFFICIENT","SCREENING_NOT_COMPLETED","SCREENING_QUEUE_UNAVAILABLE","STREAM_LIMIT_REACHED","TEST_MODE_NOT_ALLOWED","TEST_MODE_ONLY","UNAUTHORIZED","USAGE_FORMAT_INVALID","USAGE_RANGE_INVALID","USAGE_WINDOW_CONFLICT","USER_NOT_FOUND","VALIDATION_ERROR"],"description":"Stable machine code. The full catalogue (with what to do about each and whether a retry can help) is served at GET /api/v1/errors and mirrored in the `x-error-codes` extension."},"requestId":{"type":"string","description":"Correlation id for this request; also returned as the `X-Request-Id` response header. Log it: quoting it is the fastest route to a diagnosis."},"retryable":{"type":"boolean","description":"True only when repeating the identical request can succeed later without any change by the caller. Honour `Retry-After` when present; never retry a false."},"details":{"oneOf":[{"type":"object","additionalProperties":true},{"type":"array","items":{"type":"object","additionalProperties":true}}],"description":"Optional machine-readable context. `MONITOR_LIMIT_REACHED` carries `{limit, tier}`; `VALIDATION_ERROR` carries an array of `{path, message, code, expected?, received?}`, one entry per failed field."}},"required":["error"]},"ValidationErrorDetail":{"type":"object","description":"One failed field on a VALIDATION_ERROR response.","properties":{"path":{"type":"string","description":"Dotted path to the offending field (empty string for the body root).","example":"events.0.eventType"},"message":{"type":"string"},"code":{"type":"string","description":"Validation issue kind, e.g. invalid_type, too_small, invalid_enum_value."},"expected":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"What the schema wanted: a type name, or the allowed values for an enum."},"received":{"type":"string","description":"What arrived, when known."}},"required":["path","message","code"]},"OAuthError":{"type":"object","properties":{"error":{"type":"string","description":"OAuth 2.0 / OID4VCI error code, e.g. invalid_grant, invalid_proof."},"error_description":{"type":"string"}},"required":["error"]},"ScoreResult":{"type":"object","properties":{"userId":{"type":"string"},"finalScore":{"type":"number","minimum":0,"maximum":100,"nullable":true,"description":"Current canonical score (0–100); **null when no score has been computed yet**, never a placeholder value. This used to fall back to `50`, a number the engine does not produce for an unscored subject (v5.3 anchors a new record near 20, \"Unproven\") and one that bands as \"Fair\"."},"scoreBand":{"type":"string","enum":["Excellent","Good","Fair","Building","Unproven","High Risk"],"nullable":true,"description":"Band for `finalScore`; null when there is no score to band."},"confidence":{"type":"number","minimum":0,"maximum":1},"formulaVersion":{"type":"string","example":"5.3"},"computedAt":{"type":"string","format":"date-time","nullable":true}}},"SubjectSummary":{"type":"object","properties":{"externalId":{"type":"string","description":"Your own external id for the subject (test namespace stripped)."},"subjectType":{"type":"string","enum":["PERSON","AGENT","ORGANIZATION"]},"finalScore":{"type":"number","nullable":true,"description":"Current canonical score (0–100); **null when no score has been computed yet**, never a placeholder value. Filter these in or out with `hasScore`."},"scoreBand":{"type":"string","nullable":true,"enum":["Excellent","Good","Fair","Building","Unproven","High Risk"],"description":"Band for `finalScore`; null when there is no score to band."},"scoreFrozen":{"type":"boolean","description":"True when the score is frozen by a velocity flag."},"verificationDepth":{"type":"number","nullable":true,"description":"Latest snapshot verification-depth component (0–1); null when never scored."},"eventCount":{"type":"integer","description":"Count of YOUR events for this subject (your relationship, not a cross-platform total)."},"verifiedEventCount":{"type":"integer","description":"Count of YOUR verified events for this subject."},"lastActivityAt":{"type":"string","format":"date-time","nullable":true,"description":"Your most recent event for the subject; null if none."},"registeredAt":{"type":"string","format":"date-time","description":"When the subject first appeared in the ledger."},"computedAt":{"type":"string","format":"date-time","nullable":true,"description":"When the latest score was computed; null when never scored."}}},"BookSummary":{"type":"object","properties":{"formulaVersion":{"type":"string"},"matched":{"type":"integer","description":"Exact number of YOUR subjects matching the filters. Always present."},"scored":{"type":"integer","nullable":true,"description":"How many of those have a computed score. Null only when aggregation was skipped."},"unscored":{"type":"integer","nullable":true,"description":"How many are still awaiting a first score. Null only when aggregation was skipped."},"central":{"type":"object","nullable":true,"description":"Central tendency over the SCORED subjects only. Both members are null when nothing in the segment is scored: an unscored segment has no median, and a 0 there would read as a real (catastrophic) score.","properties":{"median":{"type":"number","nullable":true},"mean":{"type":"number","nullable":true}}},"bandDistribution":{"type":"array","nullable":true,"description":"Band histogram over the scored subjects (highest band first). `share` is a percentage, null when the scored population is empty.","items":{"type":"object","properties":{"band":{"type":"string","enum":["Excellent","Good","Fair","Building","Unproven","High Risk"]},"minScore":{"type":"number"},"count":{"type":"integer"},"share":{"type":"number","nullable":true}}}},"aggregationSkipped":{"type":"object","nullable":true,"description":"Present ONLY when the matched population exceeded the fold cap and the distribution was therefore not computed. `matched` is still exact; a partial aggregate is never presented as a whole-segment one.","properties":{"reason":{"type":"string","enum":["population_too_large"]},"maxSubjects":{"type":"integer"}}}}},"EventInput":{"type":"object","required":["userId","eventType"],"properties":{"userId":{"type":"string","description":"The platform's external user id (externalId)."},"eventType":{"type":"string","enum":["TRANSACTION_COMPLETED","CONTRACT_FULFILLED","REVIEW_VERIFIED","DISPUTE_RESOLVED_FOR_USER","TRANSACTION_CANCELLED","CONTRACT_CANCELLED","CONTRACT_BREACHED","TRANSACTION_DISPUTED"]},"dueDate":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time"},"stakeLevel":{"type":"string","enum":["HIGH","MEDIUM","LOW"]},"isVerified":{"type":"boolean","default":false,"description":"Whether a third party distinct from the subject confirmed this outcome. Defaults to FALSE; omitting it records the event as self-attested, which counts as activity but never as trust evidence. Sending true is a request, not a guarantee: it is reduced to false for an agent or business subject whose own operator/controller is reporting it. To have verification EARNED rather than asserted, use POST /api/v1/confirmations and let the named counterparty confirm."},"transactionValue":{"type":"number","exclusiveMinimum":0},"metadata":{"type":"object","additionalProperties":true}}},"Webhook":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["score.updated","score.band_changed","dispute.resolved","monitor.triggered","policy.threshold_crossed","usage.quota_warning","import.completed","screening.completed","confirmation.awaiting_response","confirmation.expiring_soon"]}},"description":{"type":"string","nullable":true},"isActive":{"type":"boolean"},"failureCount":{"type":"integer"},"deliveredCount":{"type":"integer"},"lastStatus":{"type":"integer","nullable":true},"lastError":{"type":"string","nullable":true},"lastDeliveryAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"RecentWebhookEvent":{"type":"object","properties":{"id":{"type":"string","description":"Event id, stable across retries and replays. Dedupe on this."},"type":{"type":"string","enum":["score.updated","score.band_changed","dispute.resolved","monitor.triggered","policy.threshold_crossed","usage.quota_warning","import.completed","screening.completed","confirmation.awaiting_response","confirmation.expiring_soon"]},"livemode":{"type":"boolean","description":"false when produced by test-mode (crd_test_) activity."},"createdAt":{"type":"string","format":"date-time"},"data":{"type":"object","additionalProperties":true,"description":"Event-specific payload; see GET /api/v1/webhooks/events."},"isExample":{"type":"boolean","description":"true = a representative example from the event catalog, NOT a delivery that occurred. Never present an example as a real outcome."},"delivery":{"type":"object","nullable":true,"description":"The delivery attempt that carried this event. Always null when isExample is true.","properties":{"id":{"type":"string"},"webhookId":{"type":"string"},"attempt":{"type":"integer"},"success":{"type":"boolean"},"statusCode":{"type":"integer","nullable":true},"deliveredAt":{"type":"string","format":"date-time"}}}}},"Monitor":{"type":"object","properties":{"id":{"type":"string"},"userId":{"type":"string","description":"The platform's external user id (externalId)."},"belowScore":{"type":"number","nullable":true,"minimum":0,"maximum":100,"description":"Fires when the score crosses DOWN through this threshold."},"aboveScore":{"type":"number","nullable":true,"minimum":0,"maximum":100,"description":"Fires when the score crosses UP through this threshold."},"onBandChange":{"type":"boolean","description":"Fires whenever the score band label changes."},"isActive":{"type":"boolean"},"lastTriggeredAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"NotificationChannel":{"type":"object","description":"A Slack or email destination that receives the SAME trust events as webhooks. For a Slack channel the `target` is MASKED in responses (the incoming-webhook URL is a bearer capability).","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["slack","email"]},"target":{"type":"string","description":"The destination. For slack, a masked hooks.slack.com URL; for email, the address."},"name":{"type":"string"},"isActive":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ConfirmationRequest":{"type":"object","properties":{"id":{"type":"string"},"subjectExternalId":{"type":"string","description":"The subject's externalId (the platform's own id). Never exposed on the public preview."},"eventType":{"type":"string","enum":["TRANSACTION_COMPLETED","CONTRACT_FULFILLED","REVIEW_VERIFIED","DISPUTE_RESOLVED_FOR_USER","TRANSACTION_CANCELLED","CONTRACT_CANCELLED","CONTRACT_BREACHED","TRANSACTION_DISPUTED"],"description":"The proposed outcome type: one of the ingestable outcomes; applied verbatim on confirm."},"stakeLevel":{"type":"string","enum":["HIGH","MEDIUM","LOW"]},"transactionValue":{"type":"number","nullable":true},"dueDate":{"type":"string","format":"date-time","nullable":true},"completedAt":{"type":"string","format":"date-time","nullable":true},"counterpartyRef":{"type":"string","description":"Opaque, platform-supplied id of the confirming counterparty. Must differ from the subject. Recorded into the resulting event metadata for velocity/graph signals; never resolved to a Credda user."},"counterpartyName":{"type":"string","nullable":true,"description":"Optional human label shown on the public preview."},"description":{"type":"string","nullable":true,"description":"Optional human sentence the counterparty sees when confirming."},"returnUrl":{"type":"string","format":"uri","nullable":true,"description":"Where the hosted confirmation page sends the counterparty after they decide. The redirect carries a status marker ONLY (`credda_confirmation=confirmed|declined`), never the token, the subject id, or anything about the record."},"status":{"type":"string","enum":["PENDING","CONFIRMED","DECLINED","EXPIRED","CANCELLED"],"description":"Only CONFIRMED writes a ledger event."},"expiresAt":{"type":"string","format":"date-time"},"resultingEventId":{"type":"string","nullable":true,"description":"The ledger event produced on confirm; null otherwise."},"decidedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"ReferenceRequest":{"type":"object","properties":{"id":{"type":"string"},"subjectExternalId":{"type":"string","description":"The subject's externalId (the platform's own id). Never exposed on the public preview."},"category":{"type":"string","enum":["education","skill","certification","employment"],"description":"The qualification category being attested (employment/education/certification/skill). Recorded verbatim on confirm."},"label":{"type":"string","nullable":true,"description":"Free-text claim label (e.g. \"Senior Engineer\", \"BSc Computer Science\"). Display only, never scored or ranked."},"issuer":{"type":"string","nullable":true,"description":"Free-text employer/institution/issuer. Display only, never scored or ranked (no prestige weighting)."},"jurisdiction":{"type":"string","nullable":true,"description":"Where the credential applies (e.g. US-TX, GB). A property of the credential, never of the person; display only."},"reference":{"type":"string","nullable":true,"description":"The issuing body's own identifier for the credential (a licence/registration/certificate number). Display only; Credda does not resolve it."},"counterpartyRef":{"type":"string","description":"Opaque, platform-supplied id of the confirming counterparty (the witness). Must differ from the subject. Passed as `verifiedBy` to the witness valve on confirm; never resolved to a Credda user."},"counterpartyName":{"type":"string","nullable":true,"description":"Optional human label shown on the public preview (the referee)."},"description":{"type":"string","nullable":true,"description":"Optional human sentence the counterparty sees when confirming."},"returnUrl":{"type":"string","format":"uri","nullable":true,"description":"Where the hosted reference page sends the counterparty after they decide. The redirect carries a status marker ONLY (`credda_confirmation=confirmed|declined`), never the token, the subject id, or anything about the record."},"status":{"type":"string","enum":["PENDING","CONFIRMED","DECLINED","EXPIRED","CANCELLED"],"description":"Only CONFIRMED records a qualification (verified). No status ever moves the reliability score."},"expiresAt":{"type":"string","format":"date-time"},"resultingEventId":{"type":"string","nullable":true,"description":"The qualification event produced on confirm; null otherwise."},"decidedAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"ThresholdPolicy":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"userId":{"type":"string","nullable":true,"description":"The subject's externalId; null for an appliesToAll policy."},"appliesToAll":{"type":"boolean","description":"When true, watches every one of your subjects (no per-subject registration)."},"metric":{"type":"string","enum":["score","component","band","verified_events"],"description":"What is watched: the score, a named component, the band, or the verified-event count."},"direction":{"type":"string","nullable":true,"enum":["up","down","enter","leave"],"description":"up/down for numeric metrics; enter/leave for a band; null = any band change."},"threshold":{"type":"number","nullable":true,"description":"The crossed value for numeric metrics (0–100 for score/component, a non-negative integer for verified_events); null for band."},"component":{"type":"string","nullable":true,"enum":["reliability","timeliness","trustworthiness","verification","consistency","momentum"],"description":"The watched component, when metric = 'component'."},"band":{"type":"string","nullable":true,"description":"The watched band, when metric = 'band'; null = any band change."},"isActive":{"type":"boolean"},"lastTriggeredAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ScreeningJob":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["QUEUED","RUNNING","COMPLETED","FAILED"]},"totalCount":{"type":"integer","description":"Deduped ids submitted."},"foundCount":{"type":"integer","nullable":true,"description":"Ids that resolved to a known user. Null until processed."},"error":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"},"startedAt":{"type":"string","format":"date-time","nullable":true},"completedAt":{"type":"string","format":"date-time","nullable":true}}},"ScreeningResultItem":{"type":"object","description":"One screened user. Score fields are present only when found.","required":["externalId","found"],"properties":{"externalId":{"type":"string","description":"The platform's external user id."},"found":{"type":"boolean"},"score":{"type":"number","minimum":0,"maximum":100},"band":{"type":"string"},"confidence":{"type":"number","minimum":0,"maximum":1},"computedAt":{"type":"string","format":"date-time"}}},"ScoreEventData":{"type":"object","description":"Payload of score.updated / score.band_changed.","properties":{"user":{"type":"object","properties":{"externalId":{"type":"string"}}},"score":{"type":"number"},"band":{"type":"string"},"previousScore":{"type":"number","nullable":true},"previousBand":{"type":"string","nullable":true},"confidence":{"type":"number"},"formulaVersion":{"type":"string"},"computedAt":{"type":"string","format":"date-time"}}},"DisputeResolvedData":{"type":"object","description":"Payload of dispute.resolved.","properties":{"disputeId":{"type":"string"},"eventId":{"type":"string","description":"Present for adjudicated resolutions; omitted for auto-lapses."},"user":{"type":"object","properties":{"externalId":{"type":"string","nullable":true}}},"outcome":{"type":"string","enum":["FOR_USER","AGAINST_USER"]},"status":{"type":"string","enum":["RESOLVED_FOR_USER","RESOLVED_AGAINST_USER"]},"lapsed":{"type":"boolean"},"resolvedAt":{"type":"string","format":"date-time"}}},"MonitorTriggeredData":{"type":"object","description":"Payload of monitor.triggered: an edge-triggered score-monitor condition fired.","properties":{"monitorId":{"type":"string"},"userId":{"type":"string","description":"The platform's external user id (externalId)."},"previousScore":{"type":"number","nullable":true},"newScore":{"type":"number"},"previousBand":{"type":"string","nullable":true},"newBand":{"type":"string"},"condition":{"type":"string","enum":["below_score","above_score","band_change"]},"threshold":{"type":"number","nullable":true,"description":"The crossed threshold; null for band_change."},"formulaVersion":{"type":"string","example":"5.3"}}},"PolicyThresholdCrossedData":{"type":"object","description":"Payload of policy.threshold_crossed: a threshold policy fired, edge-triggered. Credda supplies EVIDENCE that the line was crossed (the deterministic score context + a machine-readable decision_input block for your OWN policy engine). It is never a decision, a rating, or an instruction to act; the decision and the loss belong to you.","properties":{"policyId":{"type":"string"},"policyName":{"type":"string"},"subject":{"type":"object","properties":{"externalId":{"type":"string"}}},"metric":{"type":"string","enum":["score","component","band","verified_events"]},"condition":{"type":"object","description":"The line the policy watches.","properties":{"metric":{"type":"string","enum":["score","component","band","verified_events"]},"direction":{"type":"string","nullable":true,"enum":["up","down","enter","leave"]},"threshold":{"type":"number","nullable":true},"component":{"type":"string","nullable":true,"enum":["reliability","timeliness","trustworthiness","verification","consistency","momentum"]},"band":{"type":"string","nullable":true}}},"observed":{"type":"object","description":"The watched metric before and after the crossing (number for numeric metrics, band label for a band policy).","properties":{"previous":{},"current":{}}},"score":{"type":"number"},"previousScore":{"type":"number","nullable":true},"band":{"type":"string"},"previousBand":{"type":"string","nullable":true},"reason":{"type":"string","nullable":true,"description":"Deterministic top driver of the score change; null on the first score. Advisory."},"decision_input":{"type":"object","description":"Flat, self-contained EVIDENCE block for your policy engine: the observed crossing plus the deterministic score context that produced it. There is no verdict/outcome field, by design.","additionalProperties":true},"note":{"type":"string","description":"Restates the evidence/decision boundary on every delivery."},"formulaVersion":{"type":"string","example":"5.3"},"computedAt":{"type":"string","format":"date-time"}}},"UsageQuotaWarningData":{"type":"object","description":"Payload of usage.quota_warning: month-to-date usage crossed the warning share of the monthly quota cap. Sent at most once per quota period.","properties":{"used":{"type":"integer"},"cap":{"type":"integer"},"remaining":{"type":"integer"},"ratio":{"type":"number","description":"The configured warning share of the cap (default 0.8)."},"threshold":{"type":"integer","description":"ceil(cap × ratio), the request count that tripped the warning."},"periodEnd":{"type":"string","format":"date-time","description":"When the quota period resets (start of next UTC month)."}}},"ImportCompletedData":{"type":"object","description":"Payload of import.completed: an async historical import job (POST /api/v1/imports) reached its terminal state. Lets you stop polling the job for status. Advisory only.","properties":{"importJobId":{"type":"string"},"status":{"type":"string","enum":["COMPLETED","FAILED"]},"createdCount":{"type":"integer","description":"Rows written as new events (0 on FAILED)."},"skippedCount":{"type":"integer","description":"Rows skipped as idempotent duplicates."},"failedCount":{"type":"integer","description":"Rows that could not be mapped or written."},"error":{"type":"string","nullable":true,"description":"Terminal error message when status is FAILED; null otherwise."},"completedAt":{"type":"string","format":"date-time"}}},"ScreeningCompletedData":{"type":"object","description":"Payload of screening.completed: an async bulk screening job (POST /api/v1/screenings) reached its terminal state. Fetch full results from GET /api/v1/screenings/{id}/results. Advisory only.","properties":{"screeningJobId":{"type":"string"},"status":{"type":"string","enum":["COMPLETED","FAILED"]},"total":{"type":"integer","description":"Number of subjects screened."},"foundCount":{"type":"integer","description":"How many of them had a score on file (0 on FAILED)."},"error":{"type":"string","nullable":true,"description":"Terminal error message when status is FAILED; null otherwise."},"completedAt":{"type":"string","format":"date-time"}}},"ConfirmationReminderData":{"type":"object","description":"Payload shared by confirmation.awaiting_response and confirmation.expiring_soon: a confirmation request you created is still PENDING and either has gone unanswered past the reminder threshold or is approaching its expiry. Sent to the REQUESTING PLATFORM, at most once per request per stage. Credda delivers nothing to anyone: `counterpartyRef` is the opaque key YOU supplied, echoed verbatim and resolved to nobody, and the scoring service holds no address for a counterparty. The one-time token is NOT here and cannot be (only its hash is stored), so the payload carries URL templates with a `{confirmationToken}` placeholder you substitute from the token returned at create time. Notification only: a pending request has recorded no event, and a lapsed one records none either.","properties":{"confirmationId":{"type":"string"},"stage":{"type":"string","enum":["awaiting_response","expiring_soon"],"description":"Which reminder stage produced this event. The two stages are independent, so a short-window request can produce both."},"subject":{"type":"object","properties":{"externalId":{"type":"string"}}},"eventType":{"type":"string","description":"The outcome that would be recorded if the counterparty confirms."},"stakeLevel":{"type":"string","enum":["LOW","MEDIUM","HIGH"]},"description":{"type":"string","nullable":true,"description":"The human sentence you supplied for the counterparty to read."},"counterpartyRef":{"type":"string","description":"Your own opaque counterparty key, returned verbatim. Never resolved to a person and never contacted by Credda."},"counterpartyName":{"type":"string","nullable":true},"campaignId":{"type":"string","nullable":true,"description":"The activation campaign this request was fanned out by, if any."},"status":{"type":"string","enum":["PENDING"],"description":"Always PENDING: a terminal request is never reminded about."},"createdAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"ageHours":{"type":"number","description":"Hours the request has been waiting."},"hoursUntilExpiry":{"type":"number","description":"Hours left before the link stops working."},"confirmUrlTemplate":{"type":"string","description":"The hosted confirmation page with a `{confirmationToken}` placeholder; substitute the token you were given at create time."},"previewUrlTemplate":{"type":"string","description":"The API preview path, same `{confirmationToken}` placeholder, for platforms running their own confirmation UI."},"note":{"type":"string","description":"Restates who owns delivery and that nothing is recorded either way."}}},"UptimeWindow":{"type":"object","description":"Uptime over a window, computed from stored status snapshots: the share where every dependency (db + redis) was healthy. Snapshot-based, not continuous probing.","properties":{"uptimePercent":{"type":"number","nullable":true,"minimum":0,"maximum":100,"description":"Null when no snapshots exist in the window (unknown, never a confident 100%)."},"samples":{"type":"integer","description":"Number of snapshots the percentage is computed over."}}},"ActivityEntry":{"type":"object","properties":{"id":{"type":"string"},"action":{"type":"string","enum":["EVENT_CREATED","VELOCITY_FLAG_SET","PLATFORM_CREATED","PLATFORM_UPDATED","API_KEY_GENERATED","API_KEY_ROTATED","API_KEY_REVOKED","SHARE_TOKEN_MINTED","SHARE_TOKEN_REVOKED","CREDENTIAL_OFFER_CREATED","WEBHOOK_CREATED","WEBHOOK_UPDATED","WEBHOOK_DELETED","MONITOR_CREATED","MONITOR_UPDATED","MONITOR_DELETED","NOTIFICATION_CHANNEL_CREATED","NOTIFICATION_CHANNEL_DELETED","CONFIRMATION_REQUEST_CREATED","CONFIRMATION_REQUEST_CONFIRMED","CONFIRMATION_REQUEST_DECLINED","CONFIRMATION_REQUEST_CANCELLED","TEST_DATA_RESET","TEST_DATA_SEEDED"]},"payload":{"type":"object","additionalProperties":true,"description":"The action's recorded detail, as written. Always includes `platformId` (yours)."},"createdAt":{"type":"string","format":"date-time"}}},"ExportedEvent":{"type":"object","properties":{"id":{"type":"string"},"userId":{"type":"string","description":"The platform's own external user id (externalId)."},"eventType":{"type":"string"},"stakeLevel":{"type":"string","enum":["HIGH","MEDIUM","LOW"]},"isVerified":{"type":"boolean"},"autoImported":{"type":"boolean","description":"Convenience view of `metadata.autoImported === true` (dampened platform-sync imports)."},"transactionValue":{"type":"number","nullable":true},"dueDate":{"type":"string","format":"date-time","nullable":true},"completedAt":{"type":"string","format":"date-time","nullable":true},"daysLate":{"type":"integer","nullable":true},"createdAt":{"type":"string","format":"date-time"},"metadata":{"type":"object","additionalProperties":true}}},"VerifiedEarnings":{"type":"object","properties":{"earningsVersion":{"type":"string"},"currency":{"type":"null","description":"Always null: amounts are platform-reported units."},"note":{"type":"string"},"window":{"type":"object","properties":{"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"months":{"type":"integer"}}},"periods":{"type":"array","description":"One bucket per UTC calendar month in the window, including months with no earnings.","items":{"type":"object","properties":{"month":{"type":"string","example":"2026-06"},"grossVerified":{"type":"number"},"eventCount":{"type":"integer"},"platformBreakdown":{"type":"array","items":{"type":"object","properties":{"platform":{"type":"string"},"gross":{"type":"number"},"eventCount":{"type":"integer"}}}}}}},"attested":{"type":"object","description":"VERIFIED outcomes only. Unverified value is never included here.","properties":{"grossVerified":{"type":"number"},"eventCount":{"type":"integer"},"trailing12mTotal":{"type":"number"},"platformCount":{"type":"integer"},"platformBreakdown":{"type":"array","items":{"type":"object","properties":{"platform":{"type":"string"},"gross":{"type":"number"},"eventCount":{"type":"integer"}}}}}},"stability":{"type":"object","properties":{"monthsWithEarnings":{"type":"integer"},"medianMonthly":{"type":"number"},"meanMonthly":{"type":"number"},"coefficientOfVariation":{"type":"number","nullable":true},"longestConsecutiveMonths":{"type":"integer"}}},"unverifiedReported":{"type":"object","description":"Reported but NOT attested, clearly separated and never blended into any attested figure.","properties":{"gross":{"type":"number"},"eventCount":{"type":"integer"}}},"excluded":{"type":"object","description":"What was left out, so the omission is visible rather than silent.","properties":{"disputedEvents":{"type":"integer"},"disputedValue":{"type":"number"},"valuelessEvents":{"type":"integer","description":"Earning-shaped events carrying no usable value."}}},"coverage":{"type":"object","properties":{"verifiedShare":{"type":"number","nullable":true},"selfReportedShare":{"type":"number","nullable":true}}},"disclosures":{"type":"array","items":{"type":"string"},"description":"Always present. States what this attestation is not: not an income verification for a credit decision, not a consumer report, no representation of completeness, not a forecast."}}},"EarningsSummary":{"type":"object","description":"Compact lender-facing view. Adds no fact the full attestation does not hold.","properties":{"earningsVersion":{"type":"string","description":"Derivation version (bumped when the rules change)."},"currency":{"type":"null","description":"Always null: the ledger records no currency."},"note":{"type":"string","example":"values are platform-reported units"},"window":{"type":"object","properties":{"from":{"type":"string","format":"date-time"},"to":{"type":"string","format":"date-time"},"months":{"type":"integer"}}},"trailing12mVerifiedTotal":{"type":"number","description":"Attested gross over the 12 months ending at the window end."},"medianMonthly":{"type":"number","description":"Median of the monthly attested totals across every month in the window (zero months included)."},"monthsWithEarnings":{"type":"integer"},"volatility":{"type":"number","nullable":true,"description":"Coefficient of variation of the monthly totals. Null when there is no income to vary, never coerced to 0."},"verifiedShare":{"type":"number","nullable":true,"description":"Attested gross ÷ all reported gross. Lets a consumer judge completeness of the record. Null when nothing was reported."},"selfReportedShare":{"type":"number","nullable":true,"description":"Share of attested gross that arrived via automated platform imports rather than a counterparty confirmation."},"platformCount":{"type":"integer"},"longestConsecutiveMonths":{"type":"integer"},"disclosures":{"type":"array","items":{"type":"string"},"description":"Always present. States what this attestation is not: not an income verification for a credit decision, not a consumer report, no representation of completeness, not a forecast."}}},"ProfessionalRecord":{"type":"object","description":"A worker-owned, résumé-shaped summary of a VERIFIED work record. Describes the record the subject chose to present, never a verdict on the person.","properties":{"professionalRecordVersion":{"type":"string","example":"1.1"},"note":{"type":"string"},"reliability":{"type":"object","properties":{"score":{"type":"number"},"band":{"type":"string","description":"A factual reliability band the deterministic formula produced, not a verdict on employability."},"confidence":{"type":"number","description":"Verified-evidence confidence, 0..1."}}},"verifiedExperience":{"type":"object","properties":{"verifiedOutcomes":{"type":"integer","description":"Outcomes a third party independently confirmed."},"totalOutcomes":{"type":"integer","description":"Every recorded outcome, including self-attested."},"verificationDepth":{"type":"number","nullable":true,"description":"verifiedOutcomes ÷ totalOutcomes. Null when there is no record yet, never coerced to 0."},"verifiedPlatforms":{"type":"integer"}}},"tenure":{"type":"object","description":"The observed span of the record. Nothing is invented or projected; a missing bound is null.","properties":{"firstRecordedAt":{"type":"string","format":"date-time","nullable":true},"firstVerifiedAt":{"type":"string","format":"date-time","nullable":true},"lastRecordedAt":{"type":"string","format":"date-time","nullable":true},"trackRecordDays":{"type":"integer","nullable":true},"trackRecordMonths":{"type":"number","nullable":true}}},"status":{"type":"object","properties":{"scoreFrozen":{"type":"boolean"}}},"provenance":{"type":"object","properties":{"formulaVersion":{"type":"string"},"computedAt":{"type":"string","format":"date-time","nullable":true}}},"disclosures":{"type":"array","items":{"type":"string"},"description":"Always present. States what this record is not: not a rating/verdict on the person, not a hiring recommendation, not a background check, not a consumer report under the FCRA, and no representation of completeness."}}},"VerifiedProfile":{"type":"object","description":"How much of a claimed professional record is independently third-party verified. Deterministic and BIAS-FREE: it counts WHETHER each claim is verified, never how prestigious it is: a verified community-college credential and a verified degree from any other institution count identically; no school/employer/degree is ranked or weighted. Separate from, and unable to move, the Reliability Score.","properties":{"profileVersion":{"type":"string"},"categories":{"type":"object","description":"Per-category claimed vs verified counts + depth.","properties":{"education":{"type":"object","properties":{"claimed":{"type":"integer"},"verified":{"type":"integer","description":"Independently third-party confirmed."},"verificationDepth":{"type":"number","nullable":true,"description":"verified ÷ claimed ∈ [0,1]; null when nothing is claimed in this category."}}},"skill":{"type":"object","properties":{"claimed":{"type":"integer"},"verified":{"type":"integer","description":"Independently third-party confirmed."},"verificationDepth":{"type":"number","nullable":true,"description":"verified ÷ claimed ∈ [0,1]; null when nothing is claimed in this category."}}},"certification":{"type":"object","properties":{"claimed":{"type":"integer"},"verified":{"type":"integer","description":"Independently third-party confirmed."},"verificationDepth":{"type":"number","nullable":true,"description":"verified ÷ claimed ∈ [0,1]; null when nothing is claimed in this category."}}},"employment":{"type":"object","properties":{"claimed":{"type":"integer"},"verified":{"type":"integer","description":"Independently third-party confirmed."},"verificationDepth":{"type":"number","nullable":true,"description":"verified ÷ claimed ∈ [0,1]; null when nothing is claimed in this category."}}}}},"totals":{"type":"object","properties":{"claimed":{"type":"integer"},"verified":{"type":"integer"},"selfAttested":{"type":"integer","description":"Recorded but not independently verified."}}},"verificationDepth":{"type":"number","nullable":true,"description":"Share of the whole claimed record that is independently verified ∈ [0,1]. Equal weight per claim, no prestige. Null when nothing is claimed."},"note":{"type":"string","example":"verification of claims, not an assessment of the person"},"disclosures":{"type":"array","items":{"type":"string"},"description":"Always present. States what this is not: verification of claims, not an assessment of the person; it counts whether a claim is verified, never how prestigious it is; it is separate from the Reliability Score and can never move it; no representation of completeness."}}},"QualificationImportItem":{"type":"object","description":"The per-claim outcome of a bulk import. One entry per submitted claim, in order.","properties":{"index":{"type":"integer","description":"Position of this claim in the submitted array."},"ok":{"type":"boolean"},"category":{"type":"string","enum":["education","skill","certification","employment"]},"eventId":{"type":"string","description":"Present on success."},"eventType":{"type":"string","enum":["EDUCATION_VERIFIED","SKILL_VERIFIED","CERTIFICATION_VERIFIED","EMPLOYMENT_VERIFIED"]},"isVerified":{"type":"boolean","description":"Decided by the witness rule per claim, never hardcoded. True only when a genuine third-party witness was named."},"jurisdiction":{"type":"string","nullable":true},"reference":{"type":"string","nullable":true},"verificationNote":{"type":"string","nullable":true,"description":"Why this claim was recorded as self-attested, when applicable."},"error":{"type":"string","description":"Present only when this row failed; the rest of the batch still applied."}}},"QualificationImportResult":{"type":"object","description":"Summary of a bulk qualification import. Every item flows through the SAME single-claim writer, so isVerified is decided by the witness rule in one place and is never hardcoded. A self-import with no witnesses records every claim self-attested. Qualification claims never move the Reliability Score.","properties":{"userId":{"type":"string"},"created":{"type":"integer","description":"Number of claims recorded."},"failed":{"type":"integer"},"total":{"type":"integer"},"maxClaims":{"type":"integer","description":"The per-import cap."},"items":{"type":"array","items":{"type":"object","description":"The per-claim outcome of a bulk import. One entry per submitted claim, in order.","properties":{"index":{"type":"integer","description":"Position of this claim in the submitted array."},"ok":{"type":"boolean"},"category":{"type":"string","enum":["education","skill","certification","employment"]},"eventId":{"type":"string","description":"Present on success."},"eventType":{"type":"string","enum":["EDUCATION_VERIFIED","SKILL_VERIFIED","CERTIFICATION_VERIFIED","EMPLOYMENT_VERIFIED"]},"isVerified":{"type":"boolean","description":"Decided by the witness rule per claim, never hardcoded. True only when a genuine third-party witness was named."},"jurisdiction":{"type":"string","nullable":true},"reference":{"type":"string","nullable":true},"verificationNote":{"type":"string","nullable":true,"description":"Why this claim was recorded as self-attested, when applicable."},"error":{"type":"string","description":"Present only when this row failed; the rest of the batch still applied."}}}},"verifiedProfile":{"type":"object","description":"How much of a claimed professional record is independently third-party verified. Deterministic and BIAS-FREE: it counts WHETHER each claim is verified, never how prestigious it is: a verified community-college credential and a verified degree from any other institution count identically; no school/employer/degree is ranked or weighted. Separate from, and unable to move, the Reliability Score.","properties":{"profileVersion":{"type":"string"},"categories":{"type":"object","description":"Per-category claimed vs verified counts + depth.","properties":{"education":{"type":"object","properties":{"claimed":{"type":"integer"},"verified":{"type":"integer","description":"Independently third-party confirmed."},"verificationDepth":{"type":"number","nullable":true,"description":"verified ÷ claimed ∈ [0,1]; null when nothing is claimed in this category."}}},"skill":{"type":"object","properties":{"claimed":{"type":"integer"},"verified":{"type":"integer","description":"Independently third-party confirmed."},"verificationDepth":{"type":"number","nullable":true,"description":"verified ÷ claimed ∈ [0,1]; null when nothing is claimed in this category."}}},"certification":{"type":"object","properties":{"claimed":{"type":"integer"},"verified":{"type":"integer","description":"Independently third-party confirmed."},"verificationDepth":{"type":"number","nullable":true,"description":"verified ÷ claimed ∈ [0,1]; null when nothing is claimed in this category."}}},"employment":{"type":"object","properties":{"claimed":{"type":"integer"},"verified":{"type":"integer","description":"Independently third-party confirmed."},"verificationDepth":{"type":"number","nullable":true,"description":"verified ÷ claimed ∈ [0,1]; null when nothing is claimed in this category."}}}}},"totals":{"type":"object","properties":{"claimed":{"type":"integer"},"verified":{"type":"integer"},"selfAttested":{"type":"integer","description":"Recorded but not independently verified."}}},"verificationDepth":{"type":"number","nullable":true,"description":"Share of the whole claimed record that is independently verified ∈ [0,1]. Equal weight per claim, no prestige. Null when nothing is claimed."},"note":{"type":"string","example":"verification of claims, not an assessment of the person"},"disclosures":{"type":"array","items":{"type":"string"},"description":"Always present. States what this is not: verification of claims, not an assessment of the person; it counts whether a claim is verified, never how prestigious it is; it is separate from the Reliability Score and can never move it; no representation of completeness."}}},"note":{"type":"string"}}},"PersonalProjects":{"type":"object","description":"The subject's self-presented personal projects, each flagged verified vs self-reported. A project is never a scoring input, and the link is never fetched.","properties":{"projectsVersion":{"type":"string"},"total":{"type":"integer"},"verified":{"type":"integer","description":"Projects a third party independently confirmed."},"selfReported":{"type":"integer","description":"Recorded but not independently verified."},"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"url":{"type":"string","description":"Absolute http(s) link to a public host. Stored as text; Credda never fetches it."},"description":{"type":"string","nullable":true},"verified":{"type":"boolean","description":"True only when a genuine third-party witness confirmed the project."},"recordedAt":{"type":"string","format":"date-time"}}}},"note":{"type":"string"},"disclosures":{"type":"array","items":{"type":"string"},"description":"Always present. States what this is not: self-reported portfolio links, not a rating/ranking/verdict; verified only when a third party confirmed them; Credda stores the link and never fetches it; never a scoring input."}}},"ProjectItem":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"url":{"type":"string","description":"Absolute http(s) link to a public host. Stored as text; Credda never fetches it."},"description":{"type":"string","nullable":true},"verified":{"type":"boolean","description":"True only when a genuine third-party witness confirmed the project."},"recordedAt":{"type":"string","format":"date-time"}}},"IngestMappingDoc":{"type":"object","description":"Declares how to reach Credda event fields from YOUR record shape. Keys are Credda fields; values are field rules. `verifiedBy` is not an event field; it is the counterparty/witness identifier that licenses `isVerified: true` (without it a record is still ingested, but with `isVerified: false` plus a warning). Mappings are DATA, not code: no expressions, no templates, no scripts.","properties":{"userId":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"eventType":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"dueDate":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"completedAt":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"stakeLevel":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"isVerified":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"transactionValue":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"metadata":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"idempotencyKey":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"verifiedBy":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]}},"additionalProperties":false},"StoredMapping":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"mapping":{"type":"object","description":"Declares how to reach Credda event fields from YOUR record shape. Keys are Credda fields; values are field rules. `verifiedBy` is not an event field; it is the counterparty/witness identifier that licenses `isVerified: true` (without it a record is still ingested, but with `isVerified: false` plus a warning). Mappings are DATA, not code: no expressions, no templates, no scripts.","properties":{"userId":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"eventType":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"dueDate":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"completedAt":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"stakeLevel":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"isVerified":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"transactionValue":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"metadata":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"idempotencyKey":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]},"verifiedBy":{"oneOf":[{"type":"string","description":"Shorthand for `{ path }`, a dot-path into the record, e.g. `worker.id` or `items.0.sku`."},{"type":"object","properties":{"path":{"type":"string","description":"Dot-path into the record. Numeric segments index arrays. A missing path yields `default`, or omits the field."},"const":{"description":"A constant value, used instead of reading the record."},"values":{"type":"object","additionalProperties":true,"description":"Lookup table `{ sourceValue: creddaValue }`, applied AFTER any transforms. An unlisted source value fails that record unless `default` is set."},"default":{"description":"Fallback when the path is missing/null, or when `values` has no entry."},"transform":{"description":"One transform name, or an ordered list applied left-to-right. Fixed whitelist, no caller-supplied code.","oneOf":[{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]},{"type":"array","items":{"type":"string","enum":["cents_to_units","iso_date","lowercase","trim","boolean"]}}]}}}]}},"additionalProperties":false},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"IngestResultItem":{"type":"object","description":"One record's outcome. Records fail INDIVIDUALLY: a bad record never fails the rest.","properties":{"index":{"type":"integer","description":"Position in the submitted `records` array."},"userId":{"type":"string","description":"The mapped external user id (absent when the mapping itself failed)."},"status":{"type":"string","enum":["created","duplicate","failed"]},"eventId":{"type":"string"},"error":{"type":"string"},"warnings":{"type":"array","items":{"type":"string"},"description":"Non-fatal notes, most commonly an `isVerified` downgrade for want of counterparty evidence."}}},"ImportJob":{"type":"object","description":"One historical CSV import (status + counts). Per-row errors have their own endpoint.","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["QUEUED","RUNNING","COMPLETED","FAILED"]},"totalRows":{"type":"integer","description":"Data rows in the uploaded file (never truncated; an over-cap file is refused)."},"createdCount":{"type":"integer","description":"Rows written as new events."},"skippedCount":{"type":"integer","description":"Rows already present under their idempotency key (a safe re-upload)."},"failedCount":{"type":"integer","description":"Rows rejected. Authoritative even when the stored error list is capped."},"error":{"type":"string","nullable":true,"description":"Job-level fatal error (unparseable file, or a stall marker after a restart)."},"createdAt":{"type":"string","format":"date-time"},"startedAt":{"type":"string","format":"date-time","nullable":true},"completedAt":{"type":"string","format":"date-time","nullable":true}}},"ImportRowError":{"type":"object","properties":{"row":{"type":"integer","description":"1-based DATA row number (row 1 is the first row after the header)."},"error":{"type":"string"},"userId":{"type":"string"}}}}}}