Receive and verify
Verify raw requests, persist an inbox entry, and process events idempotently.
Authenticate the notification and durably accept work before acknowledging it. Do not perform fulfilment before protecting against duplicate delivery.
Node.js receiver
Install the published SDK. This factory returns a handler
using Request/Response. Connect acceptEvent to your application's durable inbox;
it must resolve only after commit, including when a duplicate already exists.
A background worker processes accepted entries independently.
import {
verifySignature,
WebhookSignatureVerificationError,
} from "@revkeen/sdk/webhooks";
export function createWebhookReceiver({ secret, acceptEvent }) {
if (!secret) throw new Error("Set REVKEEN_WEBHOOK_SECRET");
return async function POST(request) {
const payload = await request.text();
const signature = request.headers.get("x-revkeen-signature");
if (!signature) return new Response("Missing signature", { status: 400 });
try {
verifySignature({ payload, signature, secret, tolerance: 300 });
} catch (error) {
if (error instanceof WebhookSignatureVerificationError) {
return new Response("Invalid signature", { status: 400 });
}
throw error;
}
let event;
try {
event = JSON.parse(payload);
} catch (error) {
if (error instanceof SyntaxError) {
return new Response("Invalid JSON", { status: 400 });
}
throw error;
}
if (!event || typeof event !== "object") {
return new Response("Invalid event", { status: 400 });
}
let eventId = event.id;
if (event._truncated === true) {
if (typeof event.fetchUrl !== "string" || !URL.canParse(event.fetchUrl)) {
return new Response("Invalid event reference", { status: 400 });
}
const path = new URL(event.fetchUrl).pathname.split("/");
if (path.length !== 4 || path[1] !== "v2" || path[2] !== "events") {
return new Response("Invalid event reference", { status: 400 });
}
eventId = path[3];
} else if (typeof event.type !== "string") {
return new Response("Missing event type", { status: 400 });
}
if (typeof eventId !== "string" || eventId.length === 0) {
return new Response("Missing event ID", { status: 400 });
}
try {
await acceptEvent({ eventId, event });
} catch {
// No durable acknowledgement: ask for a retry. Alert through your own monitoring.
return new Response("Temporarily unavailable", { status: 503 });
}
return new Response(null, { status: 202 });
};
}Your framework should export the returned handler as its POST route. Pass the endpoint
secret from REVKEEN_WEBHOOK_SECRET and your storage adapter. The worker must validate
the event-specific schema before business changes. Configure an appropriate body size
limit in your HTTP server. This sample requires a maintained Node.js runtime.
Connect a durable inbox
acceptEvent is an application integration point, not an SDK function. Associate each
endpoint secret with a trusted merchant ID in your configuration; do not select a tenant
from unverified headers. A relational inbox can use (merchant_id, event_id) as a unique key:
INSERT INTO webhook_inbox (merchant_id, event_id, payload, status)
VALUES ($1, $2, $3, 'pending')
ON CONFLICT (merchant_id, event_id) DO NOTHING;Pass bound parameters and wait for transaction commit. This is a schema pattern for your application, not a migration to run against RevKeen. For a separate queue, use a transactional outbox or poll the inbox: do not acknowledge a database insert followed by a failed queue publish.
Process once per business operation
Two deliveries can both pass a separate alreadyProcessed check. A crash can occur
after an external action but before markProcessed. A unique inbox alone does not
make external effects exactly-once.
Use a business idempotency key, such as one fulfilment per invoice. Commit local state
changes and completion together. For external services, use their idempotency mechanism
and reconcile uncertain outcomes. Retry failed processing jobs independently of delivery.
A duplicate can return 2xx only when the original work is durably accepted.
Resolve truncated events
For _truncated entries, the example extracts the event ID from the signed reference
without fetching its host. Build /v2/events/{eventId} against your configured RevKeen
origin, URL-encode the identifier, and use the corresponding merchant credential.
Validate the fetched event before processing. Do not assume data.object is present
on a truncated notification or blindly fetch a URL from an incoming body.
Continue with signature details and delivery behaviour.