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 });
}
}Inbound webhook adapters
The example above covers outbound webhooks: events that CodeSpar fires to your URL when something happens in a session (a payment confirms, a label is generated, a KYC verdict lands). The signature scheme is always HMAC-SHA256 with X-CodeSpar-Signature, exactly as shown above.
The CodeSpar backend also runs inbound webhook adapters — endpoints under /v1/webhooks/<server_id>/<connection_id> that providers post to directly. There are 14+ adapters live today (10 commerce + 4 KYC: Asaas, Mercado Pago, Stripe, iugu, Stone, Coinbase Commerce, Bitso, Melhor Envio, NFe.io, Z-API plus Persona, Sift, Konduto, Truora). Each adapter verifies the provider's signature using one of four schemes:
| Scheme | Where it's used |
|---|---|
HMAC-SHA256 (hex digest in X-Signature or similar header) | Most providers — Asaas, Stripe, Z-API, etc. |
| ECDSA P-256 | Some Mercado Pago configurations |
| HTTP Basic | Sift, Konduto |
| Shared-secret comparison | Providers that pass a raw secret in body or header |
You don't write any of this verification yourself — the CodeSpar backend handles it and only forwards verified events on to your registered triggers (which are HMAC-SHA256, as documented above).
If you're self-hosting and need to verify inbound webhooks from a specific provider directly, see the per-provider section in the operations runbook.
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.
Webhook Providers Reference
Per-provider inbound webhook signature schemes — HMAC-SHA256, ECDSA P-256, HTTP Basic, shared-secret. For self-hosters and operators verifying provider events.