Webhook Listener
React to payment webhooks with a deterministic loop. On payment.confirmed, automatically issue an NF-e, create shipping, and send WhatsApp — no agent, no LLM, no surprise.
React to payment webhooks with a deterministic loop. On payment.confirmed, automatically issue an NF-e, create shipping, and send WhatsApp — no agent, no LLM, no surprise.
Prerequisites
npm install @codespar/sdk nextRegister the endpoint as a trigger via POST /v1/triggers with { name, event, webhook_url } pointing to /api/webhooks/payment. CodeSpar subscribes to the underlying provider and forwards normalized events to you, signed with X-CodeSpar-Signature. Copy the signing secret returned on creation to CODESPAR_TRIGGER_SECRET — it is only returned once.
Webhook handler
The complete handler: validate signature, create session, run the deterministic loop, return response.
import { CodeSpar } from "@codespar/sdk";
import { NextResponse } from "next/server";
import crypto from "node:crypto";
const codespar = new CodeSpar({ apiKey: process.env.CODESPAR_API_KEY! });
export async function POST(req: Request) {
// 1. Validate webhook signature — HMAC-SHA256 of the raw body
const raw = await req.text();
const signature = req.headers.get("X-CodeSpar-Signature");
const expected = crypto
.createHmac("sha256", process.env.CODESPAR_TRIGGER_SECRET!)
.update(raw)
.digest("hex");
if (!signature || signature !== expected) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
const body = JSON.parse(raw);
// 2. Only process confirmed payments
if (body.event !== "payment.confirmed") {
return NextResponse.json({ received: true });
}
// 3. Create session + run deterministic loop
const session = await codespar.create("user_123", {
servers: ["nuvem-fiscal", "correios", "z-api"],
});
try {
const result = await session.loop({
steps: [
{ tool: "codespar_invoice", params: {
type: "nfe",
customer_cpf: body.customer.cpf,
items: body.items,
total: body.amount,
}},
{ tool: "codespar_ship", params: {
to_cep: body.shipping.cep,
weight_kg: body.shipping.weight,
}},
{ tool: "codespar_notify", params: (prev) => ({
channel: "whatsapp",
to: body.customer.phone,
template: "order_confirmed",
variables: {
tracking: prev[1].data.tracking_code,
nfe_url: prev[0].data.pdf_url,
},
})},
],
abortOnError: false,
});
return NextResponse.json({
processed: true,
steps: result.completedSteps,
});
} finally {
await session.close();
}
}abortOnError: false keeps the loop running even when one step fails. The NF-e might fail, but the customer still gets their tracking code. Use onStepError to log failures without breaking the flow.
Handling failures
Capture failures with onStepError and inspect which steps succeeded via result.results:
const result = await session.loop({
steps: [...],
abortOnError: false,
onStepError: (step, error, index) => {
console.error(`Step ${index}: ${step.tool} failed`, error);
// send to Sentry, Datadog, etc.
},
});
// Queue failed steps for retry
for (const [i, stepResult] of result.results.entries()) {
if (!stepResult.success) {
await retryQueue.add({ step: i, params: stepResult.params });
}
}Idempotency
Webhooks can be delivered more than once. Use the payment ID as an idempotency key before running the loop:
const processed = await db.webhooks.findUnique({
where: { payment_id: body.payment_id },
});
if (processed) {
return NextResponse.json({ already_processed: true });
}
// ... run the loop ...
await db.webhooks.create({
data: { payment_id: body.payment_id, processed_at: new Date() },
});Many providers (Asaas, Stripe) include an idempotency_key header. When available, prefer it over the payment ID — it's scoped to the webhook delivery, not the payment event.
Next steps
Last updated on
Streaming Chat Agent
Real-time commerce chat with token-by-token streaming. Next.js App Router + Vercel AI SDK. Tool calls appear in the UI as they happen.
Multi-Tenant Agent
Build a SaaS where each customer runs their own commerce agent with their own providers and credentials. Sessions are scoped per tenant — isolated by design, metered per tenant for billing.