Reference
Receive reward webhooks
Verify the signature, persist the transaction and acknowledge safely.
Configure the destination
Configure an allowlisted HTTPS endpoint, publisher-scoped signing key reference, retry delays and timeout in callback settings. KRIN sends signed JSON POSTs. Read X-Krin-Key-Id and resolve only the approved, active key for this publisher before verification. Reject missing, unknown, revoked or wrong-publisher keys; never select a key from untrusted body fields.
Verify the exact transport
Capture raw request bytes before any JSON middleware. Pass the exact request path and query as received, without decoding, reordering or re-encoding; include the externally signed path when a trusted proxy rewrites it. Pass single-valued X-Krin-Timestamp, X-Krin-Nonce and X-Krin-Signature headers. Reject duplicate or missing headers. These functions verify HMAC and the 300-second clock window; persistent replay protection and fulfillment follow in the next step.
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
export function verifyKrin({ secret, timestamp, nonce, signature,
method, rawPathAndQuery, rawBody, now = Math.floor(Date.now() / 1000) }) {
if (!/^[0-9]{10}$/.test(timestamp) ||
Math.abs(now - Number(timestamp)) > 300 ||
typeof nonce !== "string" || !/^[^\r\n]{1,128}$/.test(nonce) ||
!/^v1=[a-f0-9]{64}$/.test(signature)) return false;
const digest = createHash("sha256").update(rawBody).digest("hex");
const canonical = [timestamp, nonce, method.toUpperCase(),
rawPathAndQuery, digest].join("\n"); // No terminal newline.
const expected = createHmac("sha256", secret).update(canonical).digest();
return timingSafeEqual(expected, Buffer.from(signature.slice(3), "hex"));
}Persist once, then acknowledge
After verification, atomically retain a nonce/signature fingerprint for at least 24 hours and durably record the exact transaction. An exact authenticated retry returns the existing receipt; changed bytes under a reused nonce are a replay conflict. Deduplicate credits by transactionId across fresh transport nonces. Parse and validate the payload, ownership and reward currency before applying it. Respond with 2xx only after durable receipt; process asynchronously from an outbox if crediting later.
Handle reversals and credit receipts
A reversal has its own transactionId, a linked originalTransactionId and signed amounts. Append a compensating entry; never edit the original credit. Keep money in USD minor units separate from publisher reward units. After your own ledger commits, call recordPublisherCreditReceipt. Callback delivery, publisher-reported credit and cash settlement describe different milestones.
Recover a delivery
Timeout, 429 and 5xx responses use the configured retry policy; bounded Retry-After is honored. Permanent 4xx responses go to dead letter. Inspect the delivery ID, status, response and latency before replaying. A replay keeps the original transaction and payload with a fresh transport nonce. It must never produce another credit.