Webhooks
Verify provider events in the merchant backend and verify outbound Carden events with a separate HMAC boundary.
On this page
Separate event directions
| Flow | Verifier | Purpose |
|---|---|---|
| Stripe or Square → merchant | Merchant backend verifies the provider signature. | Observe actual later payment changes and create normalized Carden reports. |
| Carden → merchant endpoint | Merchant endpoint verifies Carden HMAC. | Receive configured notifications about Carden activity. |
Handle provider events
- Read the raw provider request and verify it with the provider's supported routine.
- Correlate the verified event to a stable attempt and provider object IDs.
- Retrieve current provider state when notification order or content is ambiguous.
- Persist a normalized report with new eventId, actual occurredAt, cumulative amounts, source: merchant_webhook, and providerEventId.
- Deliver it from a durable worker with payments:write.
Send only allowlisted report fields. Raw provider objects can contain credentials, client secrets, customer data, or unrestricted metadata and do not belong in Carden reports.
Outbound Carden events
| Type | Meaning |
|---|---|
| payment.reported | A merchant observation was recorded. |
| enrichment.prepared | Provider context was prepared. |
| integration.synced | Integration sync finished; inspect status and exception counts. |
| integration.failed | An integration operation failed. |
| team.updated / workspace.updated | Access or workspace configuration changed. |
| webhook.test | A test delivery from the saved destination control. |
For integration.synced, inspect status, recordsRejected, and reconciliationExceptions. partially_succeeded is complete but still needs review. payment.reported means reported activity, and enrichment.prepared does not certify provider acceptance or savings.
The event envelope contains id, type, createdAt, environment, and event-specific data. Body id equals x-carden-event-id. Validate type and context and tolerate additional fields defensively.
Verify the Carden signature
x-carden-signature: t=<unix-seconds>,v1=<hex-hmac-sha256>
x-carden-event-id: <event-id>Compute HMAC-SHA256 with the webhook secret over timestamp + '.' + unmodified raw body. Parse exactly one t and one v1 value, validate their formats, apply an explicit timestamp tolerance with a synchronized clock, and compare decoded signatures in constant time before JSON parsing or side effects.
import { createHmac, timingSafeEqual } from "node:crypto";
const expected = createHmac("sha256", secret)
.update(timestamp + ".")
.update(rawBody)
.digest();
const actual = Buffer.from(signatureV1, "hex");
const valid = actual.length === expected.length && timingSafeEqual(actual, expected);Never reconstruct JSON with JSON.stringify before verification. Whitespace and byte changes alter the signature. The webhook signing secret is separate from a Carden API key.
Process idempotently and recover
- Validate signature, event type, and merchant context.
- Deduplicate using the verified body ID and confirm it equals x-carden-event-id.
- Persist or enqueue verified work before success response.
- Tolerate duplicate and out-of-order notification.
- Provide reconciliation that does not depend on webhooks as the only record.
| Delivery boundary | Current behavior |
|---|---|
| Destination | Public HTTPS on port 443 only, without URL credentials or fragments; DNS is revalidated to public addresses and redirects are refused. |
| Retry | At most six delivery attempts with exponential delays beginning at 30 seconds. |
| Secret rotation | Pending retries use the destination's current secret while immutable body and event ID stay unchanged. |
| Subscription | Enabled selected event types from the destination's organization and environment. |
No long-term replay window or strict ordering guarantee is asserted here. Build idempotent consumers and recover through reconciliation.