Skip to main content

Triggers API

HTTP API for creating, managing, and inspecting CodeSpar triggers, the outbound webhook subscriptions with signed deliveries, retries, and a dead-letter queue.

1 min read
View MarkdownEdit on GitHub

Triggers API

Base URL: https://api.codespar.dev

See the Triggers concept for the mental model (event catalog, retry semantics, signature verification). This page is the endpoint reference.

All endpoints require authentication via Bearer token and operate on the resolved project. See Authentication.


POST /v1/triggers

Create a new trigger. The signing secret is returned once on this call and on rotate-secret, never in list/get responses.

Auth required: Yes

Request body

FieldTypeRequiredDescription
namestringYesFree-form label (shown in dashboard + delivery logs)
eventstringYesExact event name to subscribe to, e.g. commerce.payment.succeeded. Lowercase, dot-separated. Wildcards are not supported; * is rejected with 400.
webhook_urlstringYesHTTPS endpoint reachable from api.codespar.dev. Private, loopback, and cloud-metadata hosts are rejected.
server_idstringNoAssociate the trigger with one catalog server (must exist in the catalog). Used as a list filter; delivery matching is by event name.

curl example

curl -X POST https://api.codespar.dev/v1/triggers \
  -H "Authorization: Bearer csk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "fulfillment-pipeline",
    "event": "commerce.payment.succeeded",
    "webhook_url": "https://yourapp.com/api/webhooks/codespar",
    "server_id": "stripe"
  }'

Response -- 201 Created

{
  "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": 0,
  "last_run_at": null,
  "created_at": "2026-04-22T14:30:00Z",
  "signing_enabled": true,
  "secret": "8c2f4b0a1d9e6c3b5a7f0e2d4c6b8a9f1e3d5c7b9a0f2e4d6c8b0a1f3e5d7c9b"
}

The secret appears only on this response. Save it immediately to your secret manager (AWS Secrets Manager, Vercel Environment Variables, 1Password, etc.). If lost, use POST /v1/triggers/:id/rotate-secret to mint a new one, which invalidates the old.


GET /v1/triggers

List triggers in the resolved project, newest first, with cursor pagination.

Auth required: Yes

Query parameters

ParameterTypeDefaultDescription
limitnumber50Results per page (max 100)
beforestring--Cursor: a trigger id from the previous page (next_before)
statusstring--Filter by active, paused, or error
eventstring--Filter by exact event name
server_idstring--Filter by associated catalog server

Response -- 200 OK

{
  "triggers": [
    {
      "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
    }
  ],
  "next_before": null
}

Pass next_before back as before to fetch the next page; it is null on the last page. secret is never included in list responses.


GET /v1/triggers/:id

Get a single trigger's current state.

Auth required: Yes

Response -- 200 OK

Same shape as a single entry in the list response. No secret.


PATCH /v1/triggers/:id

Update name, webhook URL, or status. Any field omitted is left unchanged. The event filter is immutable: create a new trigger to subscribe to a different event.

Auth required: Yes

Request body

FieldTypeDescription
namestringNew label
webhook_urlstringNew HTTPS endpoint
statusstringactive or paused (error is read-only, set by the system on auto-pause)

Use case: resume an auto-paused trigger

curl -X PATCH https://api.codespar.dev/v1/triggers/trg_abc123 \
  -H "Authorization: Bearer csk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"status": "active"}'

DELETE /v1/triggers/:id

Hard-delete the trigger. New events stop firing immediately, and pending retries for the trigger are not re-dispatched (the retry worker only claims deliveries whose trigger still exists and is active).

Auth required: Yes

Response -- 204 No Content


POST /v1/triggers/:id/rotate-secret

Mint a new signing secret. The vault entry is overwritten in place, so the old secret stops signing new deliveries immediately; deliveries already signed with it may still arrive for a short window, so verify against the new secret and fall back to the old until traffic cuts over.

Auth required: Yes

Response -- 200 OK

{
  "trigger_id": "trg_abc123",
  "signing_enabled": true,
  "secret": "5d7c9b8c2f4b0a1d9e6c3b5a7f0e2d4c6b8a9f1e3d5c7b9a0f2e4d6c8b0a1f3e"
}

If the secret store is unavailable the endpoint returns 503 vault_unavailable and no rotation occurs.


POST /v1/triggers/:id/test-fire

Push a synthetic event through the real delivery pipeline: signing, retries, DLQ, everything operates exactly as in production, and the attempt shows up in the deliveries list. The trigger must be active. Only the named trigger receives the test fire; sibling triggers subscribed to the same event stay quiet.

Auth required: Yes

Request body (optional)

FieldTypeDescription
event_typestringEvent name to fire, defaults to trigger.test_fire. Set it to rehearse a specific handler, e.g. commerce.payment.succeeded.
payloadobjectExtra keys merged into the fixture { test: true, trigger_id, requested_at }

Response -- 202 Accepted

{
  "trigger_id": "trg_abc123",
  "event_id": "evt_9f8e7d6c5b4a3210",
  "delivery_id": "412",
  "status": "delivered",
  "response_status": 200,
  "error": null
}

status reflects the synchronous first attempt: delivered, or failed (a retry is scheduled on the normal backoff). Test-firing a trigger that is not active returns 409 trigger_not_active.


GET /v1/triggers/:id/deliveries

List recent delivery attempts for a trigger, successes and failures, newest first. Each attempt is its own row.

Auth required: Yes

Query parameters

ParameterTypeDescription
limitnumberDefault 50, max 200

Response -- 200 OK

{
  "deliveries": [
    {
      "id": "412",
      "trigger_id": "trg_abc123",
      "event_id": "evt_9f8e7d6c5b4a3210",
      "attempt": 1,
      "status": "delivered",
      "response_status": 200,
      "error": null,
      "delivered_at": "2026-04-22T14:30:01Z",
      "next_retry_at": null,
      "created_at": "2026-04-22T14:30:00Z"
    },
    {
      "id": "398",
      "trigger_id": "trg_abc123",
      "event_id": "evt_1a2b3c4d5e6f7890",
      "attempt": 5,
      "status": "dead",
      "response_status": 504,
      "error": "http_504",
      "delivered_at": null,
      "next_retry_at": null,
      "created_at": "2026-04-22T10:15:00Z"
    }
  ]
}

Delivery status values: delivered, failed (retry scheduled, see next_retry_at), pending (redelivery waiting for the retry worker), dead (all 5 attempts exhausted).


GET /v1/triggers/:id/deliveries/:did

Get a single delivery with the reconstructed request and captured response, useful for DLQ debugging.

Auth required: Yes

Response -- 200 OK

{
  "delivery": {
    "id": "412",
    "trigger_id": "trg_abc123",
    "event_id": "evt_9f8e7d6c5b4a3210",
    "attempt": 1,
    "status": "delivered",
    "response_status": 200,
    "error": null,
    "delivered_at": "2026-04-22T14:30:01Z",
    "next_retry_at": null,
    "dead_at": null,
    "receipt_at": null,
    "created_at": "2026-04-22T14:30:00Z"
  },
  "request": {
    "url": "https://yourapp.com/api/webhooks/codespar",
    "sent_at_unix": 1745332200,
    "headers": {
      "Content-Type": "application/json",
      "X-CodeSpar-Event": "commerce.payment.succeeded",
      "X-CodeSpar-Event-Id": "evt_9f8e7d6c5b4a3210",
      "X-CodeSpar-Trigger-Id": "trg_abc123",
      "X-CodeSpar-Attempt": "1",
      "X-CodeSpar-Signature": "t=1745332200,v1=<redacted>"
    },
    "body": "{\"id\":\"evt_9f8e7d6c5b4a3210\",\"type\":\"commerce.payment.succeeded\",\"source\":\"stripe\",\"occurred_at\":\"2026-04-22T14:30:00Z\",\"data\":{\"payment_id\":\"pay_123\",\"amount_minor\":14900}}"
  },
  "response": {
    "status": 200,
    "body": "",
    "error": null
  },
  "event": {
    "id": "evt_9f8e7d6c5b4a3210",
    "source": "stripe",
    "event_type": "commerce.payment.succeeded",
    "payload": { "payment_id": "pay_123", "amount_minor": 14900 },
    "received_at": "2026-04-22T14:30:00Z",
    "provider_event_id": "evt_native_123"
  }
}

The request.body is rebuilt with the same serializer used at dispatch time, byte-identical to what your endpoint received, so you can re-verify the HMAC locally using sent_at_unix and your signing secret. The v1 value itself is redacted because recomputing it requires the secret.


GET /v1/triggers/:id/dlq

Dead-lettered deliveries: attempts that exhausted all 5 tries without a 2xx. Ordered by dead_at descending. Accepts the same limit parameter as /deliveries (default 50, max 200).

Auth required: Yes

Response -- 200 OK

{
  "dead_letters": [
    {
      "id": "398",
      "event_id": "evt_1a2b3c4d5e6f7890",
      "attempt": 5,
      "response_status": 504,
      "error": "http_504",
      "dead_at": "2026-04-22T13:00:00Z",
      "created_at": "2026-04-22T12:59:50Z"
    }
  ]
}

POST /v1/triggers/deliveries/:did/redeliver

Operator-driven single redelivery. Inserts a fresh pending delivery for the same event (attempt count restarts at 1); the retry worker dispatches it on its next tick. Also resets the trigger's consecutive-failure streak. It does not reactivate an auto-paused trigger; use PATCH /v1/triggers/:id with status: "active" for that.

Auth required: Yes

Response -- 202 Accepted

{
  "redelivery_id": "431",
  "trigger_id": "trg_abc123",
  "event_id": "evt_1a2b3c4d5e6f7890",
  "trigger_status": "error"
}

The redelivery carries the same X-CodeSpar-Event-Id as the original event, so an idempotency check keyed on the event id still matches. The signature is recomputed with a fresh timestamp.


POST /v1/triggers/retry-pending

Project-scoped operation: re-dispatch every delivery whose retry is due, immediately, instead of waiting for the next retry-worker tick. Useful after fixing a widespread outage. Only active triggers are drained.

Auth required: Yes

Response -- 200 OK

{
  "scanned": 47,
  "redispatched": 47,
  "delivered": 45,
  "failed": 2,
  "dead": 0
}

Errors

Common status codes for this surface. See the full Error Reference for the complete list.

StatusError codeDescription
400invalid_body / invalid_queryMissing required field or malformed value; event names must be lowercase dot-separated (* and other wildcards are rejected)
400unknown_serverserver_id is not in the catalog
400not_https / private_host / reserved_hostwebhook_url is not HTTPS or points at a private, loopback, or cloud-metadata address
401unauthorizedInvalid or missing API key
404not_foundTrigger or delivery ID does not exist in the resolved project
409trigger_not_activeTest-fire on a paused or errored trigger
503vault_unavailableSecret rotation could not be persisted; no rotation occurred
500internal_errorRetry with exponential backoff

Next steps

Triggers API | CodeSpar