Webhooks Guide
Receive real-time notifications when events happen in your storefront - queries, negotiations, transactions, and more.
Overview
Webhooks are the backbone of event-driven commerce. Instead of repeatedly polling the Cresva API to check whether something changed (pull model), webhooks let Cresva push events to your server the moment they happen (push model). This means lower latency, less wasted bandwidth, and architectures that react instantly to customer actions.
In the Agent Commerce Protocol, webhooks are especially critical because negotiations, pricing changes, and escrow state transitions happen asynchronously between AI agents and storefronts. Without webhooks, your system would need to continuously poll dozens of endpoints to stay in sync.
Push vs. Pull
Push (Webhooks)Cresva sends an HTTP POST to your server when an event occurs. No wasted requests, real-time delivery, no polling interval.Pull (Polling)Your server repeatedly calls GET endpoints. Higher latency, wasted API quota, and missed events between intervals.Webhooks are the recommended approach for any production integration. Reserve polling only for lightweight status checks where sub-second freshness is not required.
How webhooks work
When an event occurs (e.g., a negotiation is accepted or a transaction is completed), Cresva sends an HTTP POST request to your configured endpoint with the event data as a JSON payload.
- Register a webhook endpoint in your dashboard
- Cresva sends a POST request with the event payload
- Your server verifies the HMAC signature
- Your server processes the event and returns a 2xx response
Each webhook delivery includes standard headers that help you route, verify, and deduplicate events:
X-ACP-SignatureHMAC-SHA256 hex digest of the raw request body, computed with your webhook secretX-ACP-Event-TypeThe event type string, e.g. transaction.completedX-ACP-Delivery-IDA unique UUID for this specific delivery attempt. Use this for idempotency.X-ACP-TimestampUnix timestamp (seconds) of when the event was dispatchedContent-TypeAlways application/json; charset=utf-8User-AgentCresva-Webhooks/1.0Setting up an endpoint
Navigate to Settings → Webhooks in the Cresva dashboard. Add a new endpoint with:
- URL: Your HTTPS endpoint (e.g.,
https://your-app.com/webhooks/cresva) - Events: Select which event types to subscribe to, or choose "All events"
- Secret: A webhook secret will be generated - save this for signature verification
Your endpoint must respond with a 200 status code within 30 seconds, or the delivery will be marked as failed.
Choosing which events to subscribe to
Subscribing to only the events you need reduces noise and processing load. Common patterns:
Track the full lifecycle of a purchase
transaction.confirmedtransaction.paidtransaction.fulfillingtransaction.completedKeep your catalog in sync with external systems
product.out_of_stockproduct.back_in_stockproduct.updatedPower an automated counter-offer engine
negotiation.initiatednegotiation.counterednegotiation.acceptednegotiation.rejectedRecord revenue events for reconciliation
escrow.createdescrow.releasedtransaction.completedAlert on failures and disputes
query.failedtransaction.disputedtrust.updatedRegistering endpoints via the API
You can also manage webhook endpoints programmatically using the Webhooks API:
curl -X POST https://api.cresva.ai/api/storefront/[brandId]/webhooks/endpoints \
-H "Authorization: Bearer sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/cresva",
"events": [
"transaction.completed",
"negotiation.accepted",
"escrow.released"
],
"description": "Production order handler"
}'The response includes the signing_secret field. Store this securely. It is only returned once at creation time; if you lose it, rotate the secret from the dashboard.
Verifying signatures
Every webhook request includes an X-ACP-Signature header with an HMAC-SHA256 signature of the request body. Always verify this signature to ensure the webhook is authentic.
Why signature verification matters
Without verification, an attacker could send forged payloads to your endpoint, triggering fake order completions, fraudulent escrow releases, or bogus inventory changes. Always verify before processing. Use constant-time comparison functions to prevent timing attacks.
The verification algorithm is the same across all languages: compute HMAC-SHA256 of the raw request body using your webhook secret, then compare the hex digest to the X-ACP-Signature header value using a constant-time comparison.
import crypto from "crypto";
function verifyWebhookSignature(rawBody, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody, "utf-8")
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
}
// Express.js example
app.post("/webhooks/cresva", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-acp-signature"];
const isValid = verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body);
console.log("Event received:", event.type);
// Process the event...
switch (event.type) {
case "transaction.completed":
handleTransactionCompleted(event.data);
break;
case "negotiation.accepted":
handleNegotiationAccepted(event.data);
break;
}
res.status(200).send("OK");
});Important: Always read the raw request body as bytes before parsing as JSON. If your framework automatically parses JSON, it may re-serialize with different whitespace, causing signature mismatches. Use express.raw() in Express, request.data in Flask, io.ReadAll(r.Body) in Go, or request.body.read in Sinatra.
Event payload format
Every webhook event has this structure:
{
"id": "evt_1234567890",
"type": "transaction.completed",
"created_at": "2026-03-27T10:30:00Z",
"data": {
"transaction_id": "txn_x9y8z7",
"status": "COMPLETED",
"total": 178.60,
"currency": "USD",
"items": [
{
"product_id": "prod_h7k2m",
"title": "ProSound ANC-300 Wireless Headphones",
"quantity": 1,
"price": 164.99
}
]
}
}Top-level fields
idstringGlobally unique event identifier. Use this for deduplication and logging.typestringThe event type string, e.g. transaction.completed. Always in category.action format.created_atstring (ISO 8601)When the event was created, in UTC. This is the time of the event, not the delivery.dataobjectThe event-specific payload. Schema varies by event type. See examples below.Example payloads by event type
negotiation.accepted
{
"id": "evt_9876543210",
"type": "negotiation.accepted",
"created_at": "2026-03-27T09:15:00Z",
"data": {
"negotiation_id": "neg_a1b2c3",
"status": "ACCEPTED",
"product_id": "prod_h7k2m",
"original_price": 199.99,
"negotiated_price": 164.99,
"currency": "USD",
"buyer_agent_id": "agent_buyer_001",
"seller_agent_id": "agent_seller_002",
"rounds": 3,
"expires_at": "2026-03-27T10:15:00Z"
}
}escrow.released
{
"id": "evt_5544332211",
"type": "escrow.released",
"created_at": "2026-03-28T14:00:00Z",
"data": {
"escrow_id": "esc_m4n5o6",
"transaction_id": "txn_x9y8z7",
"status": "RELEASED",
"amount": 178.60,
"currency": "USD",
"released_to": "seller_account_001",
"released_at": "2026-03-28T14:00:00Z"
}
}product.out_of_stock
{
"id": "evt_6677889900",
"type": "product.out_of_stock",
"created_at": "2026-03-28T16:45:00Z",
"data": {
"product_id": "prod_h7k2m",
"title": "ProSound ANC-300 Wireless Headphones",
"previous_quantity": 3,
"current_quantity": 0,
"storefront_id": "sf_abc123"
}
}query.completed
{
"id": "evt_1122334455",
"type": "query.completed",
"created_at": "2026-03-29T08:00:00Z",
"data": {
"query_id": "qry_d7e8f9",
"status": "COMPLETED",
"agent_id": "agent_buyer_001",
"results_count": 12,
"query_text": "wireless headphones under $200 with ANC",
"duration_ms": 340,
"storefront_id": "sf_abc123"
}
}Event types
Events are grouped by resource category. Each event type follows a resource.action naming convention. Subscribe only to the events your integration needs.
Queries
query.completedquery.failedNegotiations
negotiation.initiatednegotiation.counterednegotiation.acceptednegotiation.rejectednegotiation.expirednegotiation.withdrawnTransactions
transaction.createdtransaction.confirmedtransaction.paidtransaction.fulfillingtransaction.completedtransaction.cancelledtransaction.disputedtransaction.resolvedEscrow
escrow.createdescrow.releasedescrow.disputedProducts
product.updatedproduct.out_of_stockproduct.back_in_stockPricing
price.changedOffers
offer.createdoffer.expiredoffer.claimedTrust
trust.updatedBundles
bundle.createdbundle.expiredFeedback
feedback.receivedIdempotency and duplicate handling
Webhook delivery is at-least-once, which means your endpoint may receive the same event more than once. This can happen due to network timeouts, retries, or internal replays. Your handler must be idempotent. Processing the same event twice should produce the same result as processing it once.
Strategy: track processed event IDs
Store the id field (or the X-ACP-Delivery-ID header) of every processed event. Before processing, check whether you have already handled that event.
import Redis from "ioredis";
const redis = new Redis(process.env.REDIS_URL);
app.post("/webhooks/cresva", express.raw({ type: "application/json" }), async (req, res) => {
const signature = req.headers["x-acp-signature"];
if (!verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body);
// Check for duplicate delivery
const alreadyProcessed = await redis.get(`webhook:${event.id}`);
if (alreadyProcessed) {
// Return 200 so Cresva does not retry
return res.status(200).send("Already processed");
}
// Process the event
await handleEvent(event);
// Mark as processed with a 72-hour TTL
await redis.set(`webhook:${event.id}`, "1", "EX", 259200);
res.status(200).send("OK");
});Tip: Set a TTL on your deduplication records. Cresva will not retry events older than 48 hours, so a 72-hour TTL gives a safe margin while keeping your storage clean.
Retry behavior
If your endpoint returns a non-2xx response or times out (30 seconds), Cresva retries with exponential backoff:
ImmediateFirst delivery attempt+1 minuteFirst retry+5 minutesSecond retry+30 minutesThird retry+2 hoursFourth retry+24 hoursFinal retryAfter 6 failed attempts, the endpoint is automatically disabled and an email notification is sent to the account owner. Re-enable it from the dashboard after fixing the issue.
Understanding exponential backoff
Exponential backoff increases the delay between retries to give your server time to recover. If your endpoint is down for scheduled maintenance (under 2 hours), Cresva will still deliver every event. Retries 1 through 4 will fail, but attempt 5 at +2 hours or attempt 6 at +24 hours will succeed. No events are lost during planned downtime windows shorter than 24 hours.
Each retry includes the same payload and the same event ID. The X-ACP-Delivery-ID header changes with each attempt, but the event id in the body remains constant. Use the event id for deduplication, not the delivery ID.
Error handling and response codes
How your endpoint responds determines whether Cresva marks the delivery as successful or schedules a retry.
2xx (200, 201, 202, 204)SuccessEvent is marked as delivered. No further retries.
3xx (301, 302, 307)RedirectCresva follows up to 5 redirects. Final response determines success or failure.
4xx (400, 401, 403, 404, 422)Client errorEvent is retried. Fix your endpoint and the retry will succeed. Exception: 410 Gone permanently disables the endpoint.
5xx (500, 502, 503, 504)Server errorEvent is retried. Typically indicates a temporary issue with your server.
Timeout (>30s)No responseEvent is retried. Your handler should acknowledge quickly and process asynchronously.
Best practice: acknowledge first, process later
If your event handling involves slow operations (database writes, external API calls, email sending), return 200 immediately and process the event asynchronously using a job queue.
import { Queue } from "bullmq";
const webhookQueue = new Queue("webhooks", {
connection: { host: "localhost", port: 6379 },
});
app.post("/webhooks/cresva", express.raw({ type: "application/json" }), async (req, res) => {
const signature = req.headers["x-acp-signature"];
if (!verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body);
// Enqueue for async processing — return 200 immediately
await webhookQueue.add(event.type, event, {
jobId: event.id, // Prevents duplicate jobs
attempts: 3,
backoff: { type: "exponential", delay: 5000 },
});
res.status(200).send("OK");
});Testing webhooks locally
During development, your local server is not publicly accessible. Here are several approaches to receive webhooks locally.
Cresva CLI (recommended)
The Cresva CLI can forward webhook events directly to your local development server, and lets you trigger test events on demand.
# Install the CLI
npm install -g @cresva/cli
# Login to your account
cresva login
# Forward webhooks to your local server
cresva webhooks listen --forward-to http://localhost:3000/webhooks/cresva
# In another terminal, trigger a test event
cresva webhooks trigger transaction.completed
# Trigger with a specific payload
cresva webhooks trigger negotiation.accepted --data '{"negotiation_id": "neg_test123"}'
# View recent deliveries
cresva webhooks deliveries --limit 10Using ngrok
ngrok creates a public tunnel to your local server. Register the ngrok URL as your webhook endpoint in the Cresva dashboard.
# Start your local server on port 3000
npm run dev
# In a new terminal, start ngrok
ngrok http 3000
# Copy the https://xxxxx.ngrok-free.app URL
# Add it as a webhook endpoint in Settings → Webhooks:
# https://xxxxx.ngrok-free.app/webhooks/cresvaDashboard test events
In Settings → Webhooks, click on any endpoint and use the Send test event button. Choose an event type and Cresva will send a synthetic payload to your endpoint. Test events have IDs prefixed with evt_test_ so you can filter them in your handler.
webhook.site for quick inspection
For quick debugging without setting up a server, use webhook.site to get a temporary URL that captures and displays incoming requests. Register the webhook.site URL as your endpoint, trigger events, and inspect the raw headers and body in your browser.
Security best practices
Webhook endpoints are publicly accessible HTTP handlers. Harden them with these practices.
1. Always verify signatures
Never skip signature verification, even in development. Use the constant-time comparison functions shown in the signature verification section. A simple === string comparison is vulnerable to timing attacks.
2. Require HTTPS
Cresva only delivers webhooks to HTTPS endpoints (except localhost for development). Never expose a production webhook handler over plain HTTP. The payload and signature would be visible to network intermediaries.
3. Validate the timestamp
Check the X-ACP-Timestamp header and reject events older than 5 minutes. This prevents replay attacks where an attacker captures a valid signed payload and re-sends it later.
const timestamp = parseInt(req.headers["x-acp-timestamp"], 10);
const now = Math.floor(Date.now() / 1000);
const fiveMinutes = 300;
if (Math.abs(now - timestamp) > fiveMinutes) {
return res.status(401).send("Timestamp too old — possible replay attack");
}4. IP allowlisting
For an additional layer of protection, restrict your webhook endpoint to accept requests only from Cresva IP ranges. Current webhook delivery IPs can be fetched programmatically:
curl https://api.cresva.ai/api/meta/ip-ranges | jq '.webhooks'
# Returns:
# ["52.14.0.0/16", "3.21.0.0/16", "18.191.0.0/16"]IP ranges may change. Subscribe to the infra.ip_ranges_updated notification in the dashboard to be alerted before changes take effect.
5. Rotate secrets periodically
Rotate your webhook signing secret every 90 days. Cresva supports dual-secret mode during rotation: both the old and new secrets are accepted for 24 hours, giving you time to deploy your updated secret without missing events.
6. Do not trust the payload blindly
Even after verifying the signature, validate the data before acting on it. For high-value operations (releasing funds, creating orders), fetch the canonical state from the Cresva API to confirm the event data matches the current state of the resource.
Monitoring and debugging
Visibility into webhook delivery health is critical for production systems.
Dashboard delivery log
The Settings → Webhooks → Deliveries tab shows a real-time log of all delivery attempts. For each delivery you can see:
- Event type and ID
- HTTP status code returned by your endpoint
- Response time in milliseconds
- Request headers and body (click to expand)
- Response body from your server (first 4 KB)
- Retry history and next scheduled retry
Replaying failed deliveries
After fixing an endpoint issue, you can replay failed deliveries from the dashboard or the API. Replayed events are sent with a new X-ACP-Delivery-ID but the same event id, so your idempotency logic will prevent duplicate processing.
# Replay a single failed delivery
curl -X POST https://api.cresva.ai/api/storefront/[brandId]/webhooks/deliveries/del_abc123/replay \
-H "Authorization: Bearer sk_live_your_api_key"
# Bulk replay all failed deliveries in a time range
curl -X POST https://api.cresva.ai/api/storefront/[brandId]/webhooks/deliveries/replay \
-H "Authorization: Bearer sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"endpoint_id": "we_abc123",
"status": "failed",
"created_after": "2026-03-27T00:00:00Z",
"created_before": "2026-03-28T00:00:00Z"
}'Structured logging
Log every webhook delivery on your side with structured fields for fast querying:
app.post("/webhooks/cresva", express.raw({ type: "application/json" }), (req, res) => {
const deliveryId = req.headers["x-acp-delivery-id"];
const eventType = req.headers["x-acp-event-type"];
const startTime = Date.now();
try {
const event = JSON.parse(req.body);
logger.info("webhook.received", {
delivery_id: deliveryId,
event_id: event.id,
event_type: eventType,
timestamp: event.created_at,
});
processEvent(event);
logger.info("webhook.processed", {
delivery_id: deliveryId,
event_id: event.id,
duration_ms: Date.now() - startTime,
});
res.status(200).send("OK");
} catch (err) {
logger.error("webhook.failed", {
delivery_id: deliveryId,
event_type: eventType,
error: err.message,
duration_ms: Date.now() - startTime,
});
// Return 500 so Cresva retries
res.status(500).send("Internal error");
}
});Alerting
Set up alerts on your webhook endpoint health. Monitor for: elevated error rates (more than 5% of deliveries returning non-2xx), increased response latency (p99 above 10 seconds), and disabled endpoints (Cresva sends an email and a webhook_endpoint.disabled notification when an endpoint is auto-disabled after 6 failures).
Webhook versioning and migration
Webhook payloads are versioned to ensure backward compatibility. When Cresva introduces breaking changes to a payload schema, a new version is released while the old version continues to be delivered for a deprecation period.
How versioning works
- Each webhook endpoint is pinned to the API version that was current when it was created (e.g.,
2026-03-01). - Non-breaking changes (new fields added to payloads) are applied to all versions automatically. Your handler should tolerate unknown fields.
- Breaking changes (renamed fields, removed fields, changed types) are only delivered on the new version.
- You can upgrade your endpoint version from Settings → Webhooks → Endpoint → API Version.
Migration workflow
- Review the changelog for the new version at
developers.cresva.ai/protocol/changelog. - Create a second endpoint on the new version pointing to a staging URL.
- Send test events to validate your handler against the new payload shapes.
- Update your production handler to support both old and new formats.
- Upgrade the production endpoint version in the dashboard.
- Remove backward-compatibility code after confirming all deliveries use the new version.
Deprecated versions are supported for 12 months after a new version is released. You will receive email notifications at 90, 60, and 30 days before end-of-life.
Rate limits on webhook delivery
Cresva enforces rate limits on outbound webhook delivery to protect your infrastructure from being overwhelmed during traffic spikes.
1,000 deliveries/minuteSustained delivery rate per configured endpoint10,000 deliveries/minuteAggregate across all endpoints on your account200 deliveries/secondShort burst limit per endpoint for bursty events256 KB maxEvents with larger payloads are truncated; full data available via APIWhen rate limits are exceeded, deliveries are queued and sent as capacity becomes available. No events are dropped. They are delayed. The X-ACP-Timestamp header always reflects the original event time, so you can detect delivery delays by comparing it to the current time.
If you consistently hit rate limits, consider subscribing to fewer event types per endpoint, or distributing events across multiple endpoints. Contact support for higher limits on Enterprise plans.
Troubleshooting quick reference
Ensure you are verifying against the raw request body (bytes), not a re-serialized JSON string. Check that your secret matches the one in the dashboard.
Confirm the endpoint is enabled in the dashboard. Check that you subscribed to the correct event types. Verify HTTPS is valid (no expired certificates).
Implement idempotency using the event id field. This is expected behavior; webhook delivery is at-least-once.
Your endpoint returned non-2xx for 6 consecutive attempts. Fix the issue, then re-enable from Settings → Webhooks.
Return 200 immediately and process asynchronously. Your handler has 30 seconds before Cresva considers the delivery failed.
Payloads exceeding 256 KB are truncated. Use the resource ID in the payload to fetch full data from the API.
Check your endpoint API version in Settings → Webhooks. Upgrade if you expect the latest payload schema.