API
Next.js App Router Receiver Guide
Production-ready implementation of an Asiyst webhook receiver in Next.js App Router (e.g. Caszio).
Production-Ready route.ts
Create app/api/asiyst/webhooks/route.ts in your Next.js application:
import { NextResponse, type NextRequest } from "next/server";
import { createHmac, timingSafeEqual } from "node:crypto";
const WEBHOOK_SECRET = process.env.ASIYST_WEBHOOK_SECRET || "";
// In-memory or database set to track processed event IDs
const processedEventIds = new Set<string>();
export async function POST(request: NextRequest) {
if (!WEBHOOK_SECRET) {
console.error("ASIYST_WEBHOOK_SECRET is not configured");
return NextResponse.json({ error: "Server misconfiguration" }, { status: 500 });
}
// 1. Read raw body text (do NOT parse JSON yet!)
const rawBody = await request.text();
// 2. Extract headers
const signature = request.headers.get("x-asiyst-signature") || "";
const eventId = request.headers.get("x-asiyst-event-id") || "";
const eventType = request.headers.get("x-asiyst-event-type") || "";
// 3. Verify signature
const isValid = verifySignature(WEBHOOK_SECRET, signature, rawBody);
if (!isValid) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
// 4. Parse payload
let payload: any;
try {
payload = JSON.parse(rawBody);
} catch {
return NextResponse.json({ error: "Malformed JSON" }, { status: 400 });
}
// 5. Idempotency & deduplication
const idToTrack = eventId || payload.id;
if (idToTrack && processedEventIds.has(idToTrack)) {
// Already processed: return 200 immediately
return NextResponse.json({ received: true, deduplicated: true }, { status: 200 });
}
// 6. Handle event type
try {
switch (eventType || payload.type) {
case "payment.succeeded":
console.log("Processing payment for project:", payload.project_id, payload.data);
break;
case "sdk.verified":
console.log("SDK installation verified:", payload.data);
break;
default:
console.log("Unhandled event type:", eventType);
}
if (idToTrack) processedEventIds.add(idToTrack);
return NextResponse.json({ received: true }, { status: 200 });
} catch (err) {
console.error("Failed to process webhook event:", err);
// Return 500 so Asiyst will retry according to retry policy
return NextResponse.json({ error: "Processing failure" }, { status: 500 });
}
}
function verifySignature(secret: string, signatureHeader: string, rawBody: string): boolean {
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => {
const [k, ...v] = p.trim().split("=");
return [k, v.join("=")];
})
);
const timestamp = Number(parts.t);
if (!Number.isSafeInteger(timestamp) || !parts.v1) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300) return false;
const expected = createHmac("sha256", secret).update(${timestamp}.${rawBody}).digest("hex");
const actual = Buffer.from(parts.v1, "hex");
const expectedBuf = Buffer.from(expected, "hex");
return actual.length === expectedBuf.length && timingSafeEqual(actual, expectedBuf);
}
Implementation Highlights
- Exact raw body: Calls
await request.text()before any parsing.
- Fast HTTP 200 response: Performs quick validation and returns 200 within seconds.
- Idempotency: Checks
idToTrackto safely deduplicate retries.