Subscribe once and Credda pushes a signed event the moment a score moves or a dispute resolves, with retries, replay and a full delivery log. Advisory only: no webhook can change a score.
10 event types today. Each delivery wraps the payload below in a common envelope: { id, type, createdAt, data }. The id is stable across retries and replays.
A user's Reliability Score changed, or was computed for the first time. Fires whenever the score value moves. Advisory only; receiving this never affects any score.
{
"user": {
"externalId": "seller_88"
},
"score": 72,
"band": "Good",
"previousScore": 64,
"previousBand": "Fair",
"confidence": 0.9,
"totalEvents": 18,
"verifiedEvents": 14,
"behavioralQuality": 0.86,
"formulaVersion": "5.3",
"computedAt": "2026-07-21T12:00:00.000Z",
"reason": null
}
A user crossed a score-band boundary (e.g. Fair → Good). Same payload as score.updated, but fires only when the band label actually changes.
{
"user": {
"externalId": "seller_88"
},
"score": 72,
"band": "Good",
"previousScore": 64,
"previousBand": "Fair",
"confidence": 0.9,
"totalEvents": 18,
"verifiedEvents": 14,
"behavioralQuality": 0.86,
"formulaVersion": "5.3",
"computedAt": "2026-07-21T12:00:00.000Z",
"reason": null
}
A dispute on one of your events reached a terminal outcome, decided deterministically from the record, or resolved in the user's favour after the response window lapsed. Advisory only.
{
"disputeId": "dsp_5f3a…",
"eventId": "evt_9c21…",
"user": {
"externalId": "seller_88"
},
"outcome": "FOR_USER",
"status": "RESOLVED_FOR_USER",
"lapsed": false,
"resolvedAt": "2026-07-21T12:00:00.000Z"
}
One of your score monitors fired: the user's score crossed a threshold you registered (edge-triggered: down through belowScore, up through aboveScore) or the score band changed. condition tells you which; threshold is null for band_change. Monitors are notification config only; a monitor never reads into or changes a score.
{
"monitorId": "mon_7a2c…",
"userId": "seller_88",
"previousScore": 68,
"newScore": 61,
"previousBand": "Good",
"newBand": "Fair",
"condition": "below_score",
"threshold": 65,
"formulaVersion": "5.3"
}
One of your threshold policies fired: a subject crossed the line you registered (edge-triggered: a score/component/verified-event-count crossing in a direction, or entering/leaving a band). Credda supplies EVIDENCE that the line was crossed: the deterministic score context and 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. Notification config only; a policy never reads into or changes a score.
{
"policyId": "pol_3d1e…",
"policyName": "deposit-waiver-good-band",
"subject": {
"externalId": "seller_88"
},
"metric": "band",
"condition": {
"metric": "band",
"direction": "enter",
"threshold": null,
"component": null,
"band": "Good"
},
"observed": {
"previous": "Fair",
"current": "Good"
},
"score": 72,
"previousScore": 64,
"band": "Good",
"previousBand": "Fair",
"reason": {
"scoreBefore": 64,
"scoreAfter": 72,
"scoreDelta": 8,
"direction": "up",
"topDriver": {
"factor": "OTR",
"delta": 0.12
}
},
"decision_input": {
"subject_external_id": "seller_88",
"metric": "band",
"direction": "enter",
"threshold": null,
"component": null,
"band": "Good",
"observed_previous": "Fair",
"observed_current": "Good",
"score": 72,
"previous_score": 64,
"score_band": "Good",
"previous_band": "Fair",
"components": {
"reliability": 78,
"timeliness": 71,
"trustworthiness": 96,
"verification": 60,
"consistency": 82,
"momentum": 55
},
"verified_events": 14,
"confidence": 0.9,
"formula_version": "5.3",
"computed_at": "2026-07-23T12:00:00.000Z",
"crossed_at": "2026-07-23T12:00:00.000Z"
},
"note": "Credda supplies deterministic evidence that a threshold was crossed. It is not an assessment, a rating, or an instruction to act. The decision, and the loss, belong to you.",
"formulaVersion": "5.3",
"computedAt": "2026-07-23T12:00:00.000Z"
}
Your platform's month-to-date API usage crossed the warning share of its monthly quota (default 80%), a heads-up before requests start returning 429 QUOTA_EXCEEDED. Sent at most once per quota period. Only ever fires when a monthly quota cap is configured for your tier.
{
"used": 80000,
"cap": 100000,
"remaining": 20000,
"ratio": 0.8,
"threshold": 80000,
"periodEnd": "2026-08-01T00:00:00.000Z"
}
An async historical import job (POST /api/v1/imports) reached its terminal state: COMPLETED with per-row counts, or FAILED. Lets you stop busy-polling GET /api/v1/imports/{id} for status. Advisory only; an import records events through the normal ingestion path; this notification changes no score.
{
"importJobId": "imp_4b8f…",
"status": "COMPLETED",
"createdCount": 942,
"skippedCount": 12,
"failedCount": 3,
"error": null,
"completedAt": "2026-07-24T12:00:00.000Z"
}
An async bulk screening job (POST /api/v1/screenings) reached its terminal state: COMPLETED with the found count, or FAILED. Lets you stop busy-polling GET /api/v1/screenings/{id} for status; fetch the full results from GET /api/v1/screenings/{id}/results. Advisory only; screening reads scores, it never writes one.
{
"screeningJobId": "scr_9a1c…",
"status": "COMPLETED",
"total": 500,
"foundCount": 473,
"error": null,
"completedAt": "2026-07-24T12:00:00.000Z"
}
A confirmation request you created is still PENDING and has gone unanswered past the reminder threshold (CONFIRMATION_REMINDER_AFTER_HOURS, default 48h). Sent at most once per request. Credda delivers nothing to anyone; this tells YOU to re-send the link over the channel you already own, using the one-time token you received at create time (only its hash is stored here, so the payload carries a URL template, never a working link, and never the token). Notification only: a pending request has recorded no event, and none is recorded if it lapses.
{
"confirmationId": "cnf_6b41a2c9d0e3",
"stage": "awaiting_response",
"subject": {
"externalId": "tech_88"
},
"eventType": "CONTRACT_FULFILLED",
"stakeLevel": "MEDIUM",
"description": "Confirm the emergency boiler repair on 14 March was completed",
"counterpartyRef": "client-4471",
"counterpartyName": "Northgate Dental",
"campaignId": null,
"status": "PENDING",
"createdAt": "2026-07-20T09:00:00.000Z",
"expiresAt": "2026-07-27T09:00:00.000Z",
"ageHours": 48,
"hoursUntilExpiry": 120,
"confirmUrlTemplate": "https://api.credda.io/confirm/cnf_6b41a2c9d0e3?token={confirmationToken}",
"previewUrlTemplate": "https://api.credda.io/api/v1/confirmations/cnf_6b41a2c9d0e3/preview?token={confirmationToken}",
"note": "Credda delivers nothing to anyone; you hold the channel, and the one-time token you received when this request was created. Only its hash is stored here, so this notification carries a URL template rather than a working link: substitute that token for {confirmationToken}. A request that is still pending has recorded no event, and a request that lapses records none either; an absent confirmation is not evidence of an outcome, in any direction."
}
A PENDING confirmation request is approaching its expiry (within CONFIRMATION_EXPIRING_WITHIN_HOURS, default 24h), the last window in which the counterparty can still act. Sent at most once per request, independently of confirmation.awaiting_response, so you can subscribe to only this one. Same payload, same rules: no token, no counterparty contact detail, no score, and nothing is written either way.
{
"confirmationId": "cnf_6b41a2c9d0e3",
"stage": "expiring_soon",
"subject": {
"externalId": "tech_88"
},
"eventType": "CONTRACT_FULFILLED",
"stakeLevel": "MEDIUM",
"description": "Confirm the emergency boiler repair on 14 March was completed",
"counterpartyRef": "client-4471",
"counterpartyName": "Northgate Dental",
"campaignId": null,
"status": "PENDING",
"createdAt": "2026-07-20T09:00:00.000Z",
"expiresAt": "2026-07-27T09:00:00.000Z",
"ageHours": 156,
"hoursUntilExpiry": 12,
"confirmUrlTemplate": "https://api.credda.io/confirm/cnf_6b41a2c9d0e3?token={confirmationToken}",
"previewUrlTemplate": "https://api.credda.io/api/v1/confirmations/cnf_6b41a2c9d0e3/preview?token={confirmationToken}",
"note": "Credda delivers nothing to anyone; you hold the channel, and the one-time token you received when this request was created. Only its hash is stored here, so this notification carries a URL template rather than a working link: substitute that token for {confirmationToken}. A request that is still pending has recorded no event, and a request that lapses records none either; an absent confirmation is not evidence of an outcome, in any direction."
}
Each delivery is HMAC-SHA256 signed over ${timestamp}.${rawBody}. Verify it with the SDK before trusting it. The check is constant-time and replay-tolerant. Use the raw body, not re-serialized JSON.
import { constructWebhookEvent } from '@credda/js';
// In your webhook handler, pass the RAW request body (not parsed JSON):
const event = await constructWebhookEvent({
rawBody,
signatureHeader: req.headers['x-credda-signature'],
timestampHeader: req.headers['x-credda-timestamp'],
secret: process.env.CREDDA_WEBHOOK_SECRET, // shown once, at creation
});
// Throws if the signature is invalid or the timestamp is stale.
switch (event.type) {
case 'score.updated': /* … */ break;
case 'dispute.resolved': /* … */ break;
}
Wire-compatible across the TypeScript, Python and Go SDKs, so you can sign with one and verify with another.
Create an endpoint, send a signed test delivery, then watch the cursor-paginated delivery log (one row per attempt). Failed deliveries retry with exponential backoff, and any stored delivery can be replayed.
# Create a webhook (the signing secret is returned ONCE, so store it)
$ curl -X POST https://api.credda.io/api/v1/webhooks \
-H "Authorization: Bearer $CREDDA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://you.example.com/hooks/credda",
"events":["score.updated","dispute.resolved"]}'
# Send a signed test delivery, then read the delivery log
$ curl -X POST https://api.credda.io/api/v1/webhooks/WEBHOOK_ID/test \
-H "Authorization: Bearer $CREDDA_API_KEY"
Full CRUD, delivery log and replay are in the API reference; the machine-readable event catalog is at /api/v1/webhooks/events.