Triggers
Triggers are CodeSpar's outbound webhook subscriptions. Your app receives signed HTTP callbacks when asynchronous events settle (payment succeeded, invoice issued, notification delivered) or when the platform itself changes state.
Triggers
A trigger is a CodeSpar-managed webhook subscription. You register an endpoint in your app, CodeSpar watches the provider connections in your project plus its own runtime, and when a matching event fires (commerce.payment.succeeded, commerce.invoice.issued, commerce.notify.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 and 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 a payment settling N minutes after the charge | Trigger on commerce.payment.succeeded |
| Cron-like: reconcile payments every 5 min | Poll /v1/sessions/:id/logs yourself |
| 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
- Create via
POST /v1/triggers(or in the dashboard). CodeSpar returns a signing secret once. Store it immediately; it is never revealed again. - An event fires. A provider webhook lands on one of your connections (a payment settles, an email bounces) or the platform emits an internal event (a tool call fails, a trigger auto-pauses).
- CodeSpar signs and delivers. It computes
HMAC-SHA256(secret, "<timestamp>.<body>"), putst=<unix>,v1=<hex>inX-CodeSpar-Signature, andPOSTs the event envelope to yourwebhook_url. - Your endpoint returns
2xxwithin 10 seconds. Any other outcome (non-2xx, timeout, network error) schedules a retry. - Dead-letter. After the fifth failed attempt the delivery is marked
deadand lands in the trigger's DLQ. Inspect or operator-redeliver via the API.
Trigger object
{
"id": "trg_a1b2c3d4e5f6g7h8",
"org_id": "org_xyz789",
"project_id": "prj_abc123",
"name": "fulfillment-pipeline",
"event": "commerce.payment.succeeded",
"server_id": "stripe",
"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 | Exact event name to subscribe to (see Event catalog). Lowercase, dot-separated. No wildcards: * is rejected with 400. |
server_id | Optional. Associates the trigger with one catalog server (must exist in the catalog); shown in the dashboard and usable as a list filter. Delivery matching itself is by event name. |
webhook_url | Your HTTPS endpoint, reachable from api.codespar.dev. Private, loopback, and cloud-metadata addresses are rejected. |
status | active, paused, or error (set by the system when the trigger auto-pauses after sustained delivery failures) |
signing_enabled | true when a signing secret exists for this trigger. Always true for triggers created through the managed API. |
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
Matching is exact string equality on the full event name. There are no wildcard subscriptions; to receive several events, create one trigger per event. Triggers share no state, which keeps retry semantics clean per subscription. Event names are lowercase and dot-separated; anything else (including *) is rejected with 400.
Commerce events
These originate from webhooks CodeSpar receives on your provider connections. The envelope source is the server id of the connection that produced the event (for example stripe), and CodeSpar appends connection_id and user_id to every payload so you can route the delivery to the right tenant.
| Event | Fires when |
|---|---|
commerce.payment.succeeded | The provider confirmed the payment |
commerce.payment.failed | The provider reported the payment as failed |
commerce.payment.refunded | A refund was processed (on Stripe, amount_minor carries the refunded amount) |
commerce.payment.pending | The payment is awaiting confirmation |
commerce.payment.updated | The provider updated the payment's state |
commerce.payment.disputed | The payment entered a dispute |
commerce.payment.* payload keys: provider, provider_action, provider_native_id, payment_id, amount_minor, currency, customer_ref, external_reference, raw, connection_id, user_id.
| Event | Fires when |
|---|---|
commerce.invoice.issued | The invoicing provider issued the document |
commerce.invoice.canceled | The document was canceled |
commerce.invoice.failed | The provider reported a delivery failure for the document |
Invoice events are emitted for Facturapi connections today. Note that commerce.invoice.failed means the provider could not deliver the document; it is not a fiscal rejection, and the payload carries no reason key.
| Event | Fires when |
|---|---|
commerce.kyc.approved | Identity verification approved |
commerce.kyc.rejected | Identity verification rejected |
commerce.kyc.review | Verification moved to manual review |
commerce.kyc.expired | Verification expired |
commerce.kyc.pending | Verification is pending |
commerce.kyc.* payload keys: provider, provider_action, verification_id, decision, external_reference, raw, connection_id, user_id.
| Event | Fires when |
|---|---|
commerce.notify.delivered | The channel confirmed delivery (SendGrid) |
commerce.notify.bounced | The message bounced |
commerce.notify.opened | The recipient opened the message |
commerce.notify.clicked | The recipient clicked a link in the message |
commerce.notify.* payload keys: provider, provider_action, message_id, event_type, recipient, timestamp, external_reference, raw, connection_id, user_id.
Platform events
Emitted by the CodeSpar runtime itself; the envelope source is internal.
| Event | Payload keys | Fires when |
|---|---|---|
tool_call.succeeded / tool_call.failed | tool_call_id, tool, server, duration_ms, error | A tool call completed or failed |
proxy_call.succeeded / proxy_call.failed | proxy_call_id, server, method, endpoint, upstream_status, duration_ms, error_code | A proxied provider call completed or failed |
session.closed | session_id, user_id, servers, closed_at | A session was closed |
system.health.degraded / system.health.recovered | previous_status, current_status, checks_diff, observed_at | Platform health transitioned |
trigger.paused_automatically | trigger_id, consecutive_failures, last_delivery_id | A trigger auto-paused after consecutive failed deliveries |
trigger.test_fire | test: true, trigger_id, requested_at | You called the test-fire endpoint |
Delivery format
Every delivery is a JSON envelope with a stable shape:
{
"id": "evt_9f8e7d6c5b4a3210",
"type": "commerce.payment.succeeded",
"source": "stripe",
"occurred_at": "2026-04-22T14:30:00Z",
"data": {
"provider": "stripe",
"payment_id": "pay_123",
"amount_minor": 14900,
"currency": "BRL",
"connection_id": "ca_abc123",
"user_id": "user_abc"
}
}And these headers:
POST /api/webhooks/codespar HTTP/1.1
Content-Type: application/json
X-CodeSpar-Signature: t=1745332200,v1=5f8a1c...
X-CodeSpar-Signature-Legacy: 9c1b7e...
X-CodeSpar-Event: commerce.payment.succeeded
X-CodeSpar-Event-Id: evt_9f8e7d6c5b4a3210
X-CodeSpar-Trigger-Id: trg_a1b2c3d4e5f6g7h8
X-CodeSpar-Attempt: 1X-CodeSpar-Attempt counts from 1 and increments on each retry of the same event.
Signature verification
X-CodeSpar-Signature has the form t=<unix seconds>,v1=<hex>. The v1 value is HMAC-SHA256(secret, "<t>.<raw_body>"): the timestamp from the header, a literal dot, then the raw request body. Recompute it, compare in constant time, and reject stale timestamps to defeat replays (5 minutes is a sensible tolerance):
import crypto from "node:crypto";
const TOLERANCE_SECONDS = 300;
export async function POST(req: Request) {
const raw = await req.text();
const header = req.headers.get("X-CodeSpar-Signature") ?? "";
// Header shape: t=1745332200,v1=<hex>
const parts = new Map(
header.split(",").map((kv) => kv.split("=", 2) as [string, string]),
);
const t = Number(parts.get("t"));
const v1 = parts.get("v1") ?? "";
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) {
return new Response("Stale or missing timestamp", { status: 401 });
}
const expected = crypto
.createHmac("sha256", process.env.CODESPAR_TRIGGER_SECRET!)
.update(`${t}.${raw}`)
.digest("hex");
const a = Buffer.from(v1, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return new Response("Invalid signature", { status: 401 });
}
const event = JSON.parse(raw);
// handle event.type + event.data ...
return new Response(null, { status: 200 });
}Always validate the signature against the raw request body, not the parsed JSON. JSON parse / re-serialize reorders keys and breaks the HMAC match.
X-CodeSpar-Signature-Legacy carries a bare hex HMAC computed over the body alone (no timestamp). It exists so verifiers written against the original scheme keep working during the migration window. New integrations should verify the timestamped X-CodeSpar-Signature; the legacy header will be removed.
Retries and dead-lettering
- Retry schedule: 5 attempts total (the initial delivery plus 4 retries), with waits of 1 minute, 5 minutes, 30 minutes, and 2 hours between attempts. The whole cycle wraps up within roughly nine hours, so a broken subscriber drops off the retry queue instead of retrying for days.
- Success = your endpoint returns any
2xxwithin 10 seconds of connection. Optionally include the response headerX-CodeSpar-Receipt: ackto record an explicit processing acknowledgment on the delivery record. - Attempt history: each attempt is recorded as its own delivery row, so
GET /v1/triggers/:id/deliveriesshows the full append-only history per event. - Dead-letter: an attempt that fails with no retries left is marked
dead.GET /v1/triggers/:id/dlqlists every dead delivery with its response status and error so you can replay locally. - Auto-pause: 20 consecutive dead-lettered deliveries flips the trigger's
statustoerrorand emits atrigger.paused_automaticallyevent (subscribe to it with a second trigger if you want to be alerted). Retries only run foractivetriggers, so an auto-paused trigger stops delivering entirely. Clear the underlying issue, thenPATCH /v1/triggers/:idwithstatus: "active"to resume. - Operator redeliver:
POST /v1/triggers/deliveries/:did/redeliverre-enqueues a single delivery.POST /v1/triggers/retry-pendingre-dispatches every due retry in your project immediately instead of waiting for the next worker tick.
Idempotency
CodeSpar guarantees at-least-once delivery: the same event may arrive more than once under retry or operator redelivery. Use X-CodeSpar-Event-Id as your idempotency key. Retries and redeliveries of the same event carry the same event id, so a dedupe check on it collapses them:
const eventId = req.headers.get("X-CodeSpar-Event-Id");
if (await alreadyProcessed(eventId)) {
return new Response(null, { status: 200 });
}
await processEvent(event);
await markProcessed(eventId);Test-fire
POST /v1/triggers/:id/test-fire sends a synthetic trigger.test_fire event through the real signing and delivery pipeline, and the attempt shows up in the deliveries list like any other. Pass event_type in the body to rehearse a specific handler (for example commerce.payment.succeeded); pass payload to merge extra keys into the fixture { test: true, trigger_id, requested_at }.
curl -X POST https://api.codespar.dev/v1/triggers/trg_abc123/test-fire \
-H "Authorization: Bearer csk_live_..."The trigger must be active; test-firing a paused or errored trigger returns 409. Test fires reach only the trigger you name, so sibling triggers subscribed to the same event stay quiet.
Next steps
Tools & Meta-Tools
CodeSpar provides 15 meta-tools that abstract every connected MCP server into a unified commerce interface, reducing context window cost and simplifying agent development.
Authentication
API key management, service authentication, key rotation, and security best practices for the CodeSpar API.