Skip to main content
Karsa webhooks notify your backend when KYB processing changes after an API request has already returned. Webhooks are configured for an ingestion source, not for each Customer, and are optional for synchronous customer and business-onboarding requests. This guide walks through the complete integration:
  1. Build an HTTPS receiver that preserves the raw request body.
  2. Add the endpoint and choose its event subscriptions.
  3. Install the Karsa-generated Ed25519 public key in your receiver.
  4. Complete Karsa’s endpoint-verification challenge.
  5. Send a test event and inspect its delivery history.
  6. Enable production event processing and handle retries safely.

Before you begin

You need:
  • A source API credential or access to Settings → Integrations in Karsa.
  • A publicly reachable HTTPS endpoint for production.
  • A receiver that can read the unmodified request bytes and return a 2xx quickly.
  • A secure location for the Ed25519 public key and endpoint metadata.
Source API keys can manage endpoints belonging to their source. Machine OIDC credentials must include karsa:webhooks:manage in both the issued token and the tenant service-account grant.
You can create multiple destinations for one ingestion source and select subscriptions separately for each destination.

Events available in version 1

KYB payloads contain source identity, customer subject ID, evaluation ID, workflow-run ID, status, and a bounded outcome or failure code. data.kybRun repeats the run as Trigger a KYB run documents it: its public status, customer IDs, result summary, and failure code. They exclude customer profiles, documents, raw provider evidence, model output, credentials, and signing secrets.

1. Obtain the verification public key

Karsa generates a dedicated Ed25519 signing-key pair when you create an endpoint. Karsa retains the private key encrypted in its backend and never accepts, returns, or exposes private signing-key material through the API or web app. Copy the public key, SHA-256 fingerprint, and key version from the created endpoint into the configuration used by your receiving service. The public key is verification material, not a shared secret. Compare its fingerprint when moving it between environments.
You do not generate or upload a key pair. Possession of Karsa’s public key lets your backend verify signatures but cannot be used to create valid Karsa signatures.

2. Implement the receiver

Create a POST route that:
  1. Reads the request as raw bytes.
  2. Extracts the Karsa signature headers.
  3. Selects the public key identified by Karsa-Webhook-Key-Version.
  4. Verifies the Ed25519 signature before parsing JSON.
  5. Returns the verification challenge when the event type is webhook.endpoint.verification.
  6. Stores the immutable event ID as a deduplication key.
  7. Queues business processing and returns a 2xx promptly.

Express example

Replace acceptWebhookEventOnce with your durable inbox or queue operation. Do not acknowledge a new event until it has been durably accepted, and return a 2xx for an event ID already accepted.
JSON middleware must not run before signature verification. Parsing and reserializing the payload changes its bytes and invalidates the signature.

3. Add the endpoint in Karsa

In the web app:
  1. Open Settings → Integrations.
  2. Select the ingestion source that owns the endpoint.
  3. In Webhooks, choose Add endpoint.
  4. Enter a recognizable name and the receiver’s HTTPS URL.
  5. Select the KYB event types the destination should receive.
  6. Save the endpoint.
  7. Copy the generated public key into your receiver configuration.
The endpoint starts in PENDING_VERIFICATION. Karsa does not enqueue normal KYB deliveries for it until verification succeeds. Changing an active endpoint URL returns it to PENDING_VERIFICATION. Changing a disabled endpoint URL keeps it DISABLED, clears its prior verification, and requires an explicit enable followed by verification. Changing only its name or event subscriptions preserves its verification state. Disabling an endpoint stops new events from being queued for that destination; already queued deliveries continue using their pinned URL and signature.

API alternative

You can create the same endpoint with a source-authenticated request:
The response contains the generated public key, fingerprint, key version, and endpoint status. It never contains private signing-key material.

4. Verify the endpoint

Choose Verify endpoint in the web app or call:
Karsa sends a signed webhook.endpoint.verification request to the configured URL. After verifying the signature, return a 2xx JSON response containing the exact challenge from the body:
Karsa activates the endpoint only if the challenge succeeds and the URL and active signing key did not change while verification was in progress. Otherwise the endpoint stays unverified and the UI shows the failure reason. A disabled endpoint cannot be verified. Enable it deliberately before verification; a verification request never changes DISABLED back to ACTIVE. If its URL changed while disabled, enabling it moves it to PENDING_VERIFICATION instead of activating it.

5. Send a test event

After verification, choose Send test. The test uses the normal event ledger, signing code, durable delivery workflow, retry rules, and history—not a separate connectivity check. Your receiver gets a webhook.test event. Confirm that:
  • Signature verification succeeds against the advertised key version.
  • The header event ID and event type match the JSON body.
  • The event ID is stored as a deduplication key.
  • Your receiver returns a 2xx after durable acceptance.
  • The Karsa delivery history shows DELIVERED.
If delivery fails, open the endpoint’s recent attempts to see its HTTP status, bounded failure explanation, duration, retry state, and next attempt. Payloads, private keys, and signatures are not written to application logs.

Request headers

Event envelope

Every KYB delivery uses a stable version 1 envelope:
Use id for deduplication. Use createdAt and your own domain rules when reconciling state because events for different customers and endpoints are not globally ordered.

Delivery and retry behavior

Delivery is at least once. Your receiver can observe a duplicate if it accepted a request but Karsa could not record that acceptance. Automatic retries continue for up to 24 hours by default. The event ID, raw body, signing timestamp, key version, and signature remain byte-for-byte identical across automatic and manual retries. Searchable history is available for 30 days. Because the timestamp is intentionally stable across retries, use the immutable event ID—not a short arrival-time cutoff—as the primary replay defense. You can enforce a timestamp policy that matches your integration, but it must allow Karsa’s documented retry window or valid retries will be rejected. Karsa re-resolves and validates DNS before every attempt, requires HTTPS in production, blocks URL credentials and private, loopback, link-local, documentation, multicast, and cloud-metadata address ranges, and caps response time and response-body size.

Which endpoints receive an event

When a KYB lifecycle event (kyb.evaluation.queued, kyb.evaluation.completed, or kyb.evaluation.failed) occurs, Karsa creates a delivery for every endpoint that is ACTIVE and subscribed to that event type at that moment. Events are not queued for endpoints that are PENDING_VERIFICATION or DISABLED, and they are not sent automatically when the endpoint is verified or enabled. Finish verification before you trigger work whose webhooks you need, or send the events it missed afterwards with Replay missed events. A webhook.test event is different: it is sent only to the endpoint you request it for, which must be ACTIVE. A queued delivery keeps the destination URL, raw body, timestamp, and signature it was created with, so deliveries already queued continue to retry the previous URL until they succeed or dead-letter. Changing the URL of an ACTIVE endpoint moves it back to PENDING_VERIFICATION: the new URL receives only events that occur after it is verified, and events in between are not sent to either URL. Treat webhooks as notifications and the API as the source of truth. After downtime, a pending or disabled endpoint, or a URL change, list the events you missed with GET /api/ingestion/webhook-events as described in Catch up on missed events. For a known run, GET /api/ingestion/kyb-runs/{id} returns its current state.

Catch up on missed events

Karsa records every KYB lifecycle event for your ingestion source when it occurs, whether or not an endpoint received it. GET /api/ingestion/webhook-events lists those events for the calling source credential, oldest first. Each item has the same fields and values as the envelope that was (or would have been) delivered, including the same id as Karsa-Webhook-Id, so feed it through the same inbox and deduplication as webhook deliveries. JSON key order can differ from the delivered body, and items carry no signature: the authenticated API response is the proof of origin, so do not run webhook signature verification on them.
  • Pass nextCursor as cursor to read the next page. nextCursor is null on the last page.
  • type filters by event type; repeat it or separate values with commas. webhook.test events are excluded unless you request them. Endpoint verification challenges are not recorded.
  • Events are listed for 30 days, the same window as delivery history. Older events and a since before that window are not returned.
  • To resume later, pass the createdAt of the last event you stored as since, starting a few minutes earlier: an event can become visible shortly after its createdAt. Skip IDs you already stored.
GET /api/ingestion/webhook-events/{eventId} returns one event, or 404 with WEBHOOK_EVENT_NOT_FOUND when it does not belong to your source or is outside the window.

Replay missed events

After an endpoint is verified or re-enabled, you can have Karsa deliver the events it missed instead of reading them from the events API. Choose Replay missed events on the endpoint in the web app (supervisors only), or call:
  • Karsa queues a delivery to this endpoint for every recorded event of your source that was created at or after since, has a type the endpoint subscribes to now, and was never queued for this endpoint. Events the endpoint already has a delivery for, in any state, are counted in skippedAlreadyDelivered and not sent again; use manual retry for dead-lettered ones. webhook.test events are never replayed.
  • The endpoint must be ACTIVE; otherwise the call returns 409 with WEBHOOK_NOT_ACTIVE.
  • since is limited to the 30-day history window; an earlier value starts at the window.
  • Each call queues at most 500 events, oldest first. When capped is true, call again with the same since to queue the rest. Repeating a call never queues an event twice.
  • A replayed delivery has the original event ID in Karsa-Webhook-Id and the original envelope as its body, so your inbox deduplicates it like any retry. JSON key order can differ from the body other endpoints received.
  • A replayed delivery is signed when it is queued, with the endpoint’s current active key, and Karsa-Webhook-Timestamp is that time rather than the event’s createdAt. A replay is a new delivery with its own full retry window, and within it every attempt keeps the same timestamp and signature, so a timestamp policy that allows the retry window also accepts replays of older events.
  • OIDC service principals need the karsa:webhooks:manage scope. The call is audit-logged and shares your source’s request rate limit.

Duplicate handling and ordering

Apply a unique database constraint to the event ID. A safe receiver transaction is:
  1. Verify the signature and header/body agreement.
  2. Insert the event ID and raw payload into an inbox table with a unique constraint.
  3. If the event ID already exists, return a 2xx without applying it again.
  4. Commit the inbox record.
  5. Process the event asynchronously and idempotently.
Do not rely on delivery order across endpoints or customers. For multiple events affecting the same subject, compare createdAt and the lifecycle state rather than assuming the last request received is the newest state.

Rotate signing keys

Choose Rotate key in the web app or use the rotate-key API. Karsa generates and retains the new private key and publishes the new public key version. The new version becomes active immediately. The previous public key remains in OVERLAP for at least the configured delivery retry window—24 hours by default and configurable up to 168 hours. The web app requests the full 168-hour overlap so rotation remains safe for every supported retry configuration. API clients may choose a shorter overlap only when it is at least the environment’s configured retry window. During overlap:
  • Keep both public-key versions in your receiver’s key map.
  • Select the key using Karsa-Webhook-Key-Version.
  • Do not replace the old key until its overlap status becomes RETIRED.
  • Remember that previously queued deliveries remain pinned to their original key and signature.

Replay a failed delivery

When a delivery reaches DEAD_LETTER, choose Retry in its delivery history. Manual retry does not create a new event. It preserves the original event ID, raw body, timestamp, key version, and signature, and creates another append-only delivery attempt. Deleting an endpoint stops new events and marks pending deliveries as dead-lettered. The deleted endpoint remains as a read-only archive in the endpoint list for the configured history-retention period so analysts can still inspect its delivery and attempt history; deleted deliveries cannot be retried.

Troubleshooting

Go-live checklist

  • The production URL uses HTTPS and does not redirect.
  • Raw-body signature verification runs before JSON parsing.
  • Public keys are selected by key version and stored securely.
  • Header event ID/type are compared with the body.
  • Event IDs have a unique database constraint.
  • Events are durably queued before returning 2xx.
  • Processing is asynchronous and idempotent.
  • Both active and overlapping public keys are accepted during rotation.
  • Delivery failures and dead-lettered events are monitored.
  • A test event succeeds from Karsa and appears in delivery history.