---
title: Triggers
description: Triggers are CodeSpar's outbound webhook subscriptions — your app receives signed HTTP callbacks when asynchronous commerce events settle (payment confirmed, invoice authorized, shipment in transit, message delivered).
---

import { Callout } from "fumadocs-ui/components/callout";

# Triggers

A **trigger** is a CodeSpar-managed webhook subscription. You register an endpoint in your app, CodeSpar watches the underlying provider (Stripe, Asaas, Correios, etc.), and when a matching event settles — `payment.confirmed`, `invoice.authorized`, `shipment.in_transit`, `message.delivered` — CodeSpar sends a signed HTTP POST to your endpoint.

Triggers replace the traditional pattern of wiring up one webhook per provider: one endpoint, normalized payload shape, single signing secret, single retry / dead-letter queue.

## When to use a trigger vs `session.send` / `session.execute`

| You want to... | Use |
|---|---|
| Agent asks "charge R$150 via Pix" — synchronous result | `session.execute` / `session.send` |
| Background job: react to "payment confirmed" N minutes after charge | Trigger |
| Cron-like: reconcile Pix every 5 min | Poll `/v1/sessions/:id/logs` yourself, or a Trigger on `payment.reconciled` |
| In-conversation "send the checkout link" | `session.execute("codespar_notify", ...)` — not a trigger |

Triggers are the async half of the Complete Loop. Whenever an action your agent kicks off today produces a settlement event tomorrow, a trigger is the right primitive.

## Lifecycle

1. **Create** via `POST /v1/triggers` (or in the dashboard). CodeSpar returns a signing secret **once** — store it immediately; it is never revealed again.
2. **Event fires** somewhere in your session logs (a Pix from `codespar_pay` settles, an NF-e from `codespar_invoice` is authorized).
3. **CodeSpar signs** the payload with `HMAC-SHA256(secret, raw_body)`, puts the hex digest in `X-CodeSpar-Signature`, and `POST`s to your `webhook_url`.
4. **Your endpoint returns `2xx`** within 10 seconds. Any other response (non-2xx, timeout, network error) is retried with exponential backoff up to 12 attempts.
5. **Dead-letter** — after 12 failed attempts the delivery lands in the trigger's DLQ. Inspect or operator-redeliver via the API.

## Trigger object

```json
{
  "id": "trg_a1b2c3d4e5f6g7h8",
  "org_id": "org_xyz789",
  "name": "fulfillment-pipeline",
  "event": "payment.confirmed",
  "server_id": "asaas",
  "webhook_url": "https://yourapp.com/api/webhooks/codespar",
  "status": "active",
  "total_runs": 1847,
  "last_run_at": "2026-04-22T14:30:00Z",
  "created_at": "2026-04-01T09:00:00Z",
  "signing_enabled": true
}
```

| Field | Description |
|-------|-------------|
| `id` | Trigger ID in the form `trg_<16chars>` |
| `name` | Free-form label shown in the dashboard and logs |
| `event` | Event filter — `*` matches all, or a specific event name (see [Event catalog](#event-catalog)) |
| `server_id` | Optional — restrict to events originating from one MCP server |
| `webhook_url` | Your HTTPS endpoint. Must be reachable from `api.codespar.dev` |
| `status` | `active`, `paused`, or `error` (auto-paused after sustained delivery failures) |
| `signing_enabled` | Always `true` for triggers created through the managed API. Indicates a signing secret exists in the vault. |

The `secret` field is returned **only** on `POST /v1/triggers` (creation) and `POST /v1/triggers/:id/rotate-secret` (rotation). Never in list or get responses.

## Event catalog

The event string is namespaced `<domain>.<action>`. Events that fire today:

| Event | Fires when |
|-------|-----------|
| `payment.confirmed` | Provider confirmed settlement (Pix, boleto, card capture, SPEI) |
| `payment.failed` | Charge permanently failed after retries |
| `payment.refunded` | Refund settled back to the original payment method |
| `payout.settled` | Outbound payout reached the destination account |
| `invoice.authorized` | NF-e / NFS-e / CFDI authorized by the tax authority |
| `invoice.rejected` | Fiscal authority rejected the document (see `reason` field in payload) |
| `shipment.label_created` | Shipping label purchased, tracking code available |
| `shipment.in_transit` | Carrier scan — object left origin |
| `shipment.delivered` | Carrier confirmed final delivery |
| `message.delivered` | WhatsApp / SMS / email confirmed by the channel |
| `message.read` | User opened the message (where the channel reports it) |
| `connection.expired` | OAuth token expired — no more tool calls until re-authorized |

Pass `event: "*"` to receive everything, or one specific event to filter. For multi-event subscriptions, create multiple triggers — they share no state, which keeps retry semantics clean.

## Signature verification

Every delivery includes these headers:

```
POST /api/webhooks/codespar HTTP/1.1
Content-Type: application/json
X-CodeSpar-Signature: 7f9b4c...
X-CodeSpar-Event: payment.confirmed
X-CodeSpar-Delivery-Id: dlv_m3n4o5p6
X-CodeSpar-Timestamp: 2026-04-22T14:30:00Z
```

Verify with `HMAC-SHA256(secret, raw_body)` and a constant-time compare:

```typescript
import crypto from "node:crypto";

export async function POST(req: Request) {
  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 || !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    return new Response("Invalid signature", { status: 401 });
  }

  const event = JSON.parse(raw);
  // handle event.type + event.data ...
  return new Response(null, { status: 200 });
}
```

<Callout type="warn">
Always validate the signature against the **raw request body**, not the parsed JSON. JSON parse / re-serialize reorders keys and breaks the HMAC match.
</Callout>

## Retries and dead-lettering

- **Retry schedule**: 12 attempts over roughly 24 hours with exponential backoff (10s, 30s, 2min, 10min, 30min, 1h, 2h, 4h, 6h, 8h, 10h, 12h).
- **Success** = your endpoint returns any `2xx` within 10 seconds of connection.
- **Auto-pause**: 50 consecutive failed deliveries across one trigger flips `status` to `paused`. Clear the underlying issue then `PATCH /v1/triggers/:id` with `status: "active"` to resume.
- **DLQ inspection**: `GET /v1/triggers/:id/dlq` lists every delivery that exhausted retries. The response includes the full request + last response body so you can replay locally.
- **Operator redeliver**: `POST /v1/triggers/deliveries/:did/redeliver` re-enqueues a single delivery. Useful for one-off fixes; do not loop it in production.

## Idempotency

CodeSpar guarantees **at-least-once** delivery — the same delivery may arrive more than once under retry or during network partitions. Use `X-CodeSpar-Delivery-Id` as your idempotency key:

```typescript
const deliveryId = req.headers.get("X-CodeSpar-Delivery-Id");
if (await alreadyProcessed(deliveryId)) {
  return new Response(null, { status: 200 });
}
await processEvent(event);
await markProcessed(deliveryId);
```

## Test-fire

`POST /v1/triggers/:id/test-fire` pushes a synthetic event through the real delivery pipeline (with retries, signing, DLQ — everything). Good for verifying your endpoint ahead of a production release.

```bash
curl -X POST https://api.codespar.dev/v1/triggers/trg_abc123/test-fire \
  -H "Authorization: Bearer csk_live_..."
```

The payload uses a fixture shape that matches the subscribed event; your endpoint should respond exactly as it would in production.

## Next steps

<NextStepsGrid items={[
  { label: "REFERENCE", title: "Triggers API", description: "Full HTTP reference — create, rotate, test-fire, DLQ.", href: "/docs/api/triggers" },
  { label: "COOKBOOK", title: "Webhook Listener", description: "End-to-end cookbook: subscribe, validate, run a deterministic loop.", href: "/docs/cookbooks/webhook-listener" },
  { label: "CONCEPT", title: "Sessions", description: "The synchronous counterpart — session.send / execute.", href: "/docs/concepts/sessions" },
  { label: "DEBUGGING", title: "Debugging", description: "Inspect delivery attempts + DLQ + tool-call logs.", href: "/docs/debugging" },
  { label: "ERRORS", title: "Error Reference", description: "Every error code the API can return, in one place.", href: "/docs/errors" },
]} />
