Skip to content
Cresva
Developers

Webhooks Guide

Receive real-time notifications when events happen in your storefront - queries, negotiations, transactions, and more.

Webhooks in preview
Webhook delivery infrastructure (endpoint store, signed delivery, retry worker) is live in the Cresva backend. The public registration UI and management endpoints are not yet exposed — the dashboard controls and management cURL examples below describe the planned surface. The HMAC-SHA256 signature scheme and event-payload shapes documented here are stable.

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.

  1. Register a webhook endpoint in your dashboard
  2. Cresva sends a POST request with the event payload
  3. Your server verifies the HMAC signature
  4. 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 secret
X-ACP-Event-TypeThe event type string, e.g. transaction.completed
X-ACP-Delivery-IDA unique UUID for this specific delivery attempt. Use this for idempotency.
X-ACP-TimestampUnix timestamp (seconds) of when the event was dispatched
Content-TypeAlways application/json; charset=utf-8
User-AgentCresva-Webhooks/1.0

Setting 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:

Order fulfillment

Track the full lifecycle of a purchase

transaction.confirmedtransaction.paidtransaction.fulfillingtransaction.completed
Inventory sync

Keep your catalog in sync with external systems

product.out_of_stockproduct.back_in_stockproduct.updated
Negotiation bot

Power an automated counter-offer engine

negotiation.initiatednegotiation.counterednegotiation.acceptednegotiation.rejected
Finance / accounting

Record revenue events for reconciliation

escrow.createdescrow.releasedtransaction.completed
Monitoring

Alert on failures and disputes

query.failedtransaction.disputedtrust.updated

Registering endpoints via the API

You can also manage webhook endpoints programmatically using the Webhooks API:

bash
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.

javascript
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:

JSON
{
  "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

JSON
{
  "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

JSON
{
  "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

JSON
{
  "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

JSON
{
  "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.failed

Negotiations

negotiation.initiatednegotiation.counterednegotiation.acceptednegotiation.rejectednegotiation.expirednegotiation.withdrawn

Transactions

transaction.createdtransaction.confirmedtransaction.paidtransaction.fulfillingtransaction.completedtransaction.cancelledtransaction.disputedtransaction.resolved

Escrow

escrow.createdescrow.releasedescrow.disputed

Products

product.updatedproduct.out_of_stockproduct.back_in_stock

Pricing

price.changed

Offers

offer.createdoffer.expiredoffer.claimed

Trust

trust.updated

Bundles

bundle.createdbundle.expired

Feedback

feedback.received

Idempotency 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.

javascript
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:

Attempt 1ImmediateFirst delivery attempt
Attempt 2+1 minuteFirst retry
Attempt 3+5 minutesSecond retry
Attempt 4+30 minutesThird retry
Attempt 5+2 hoursFourth retry
Attempt 6+24 hoursFinal retry

After 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)Success

Event is marked as delivered. No further retries.

3xx (301, 302, 307)Redirect

Cresva follows up to 5 redirects. Final response determines success or failure.

4xx (400, 401, 403, 404, 422)Client error

Event is retried. Fix your endpoint and the retry will succeed. Exception: 410 Gone permanently disables the endpoint.

5xx (500, 502, 503, 504)Server error

Event is retried. Typically indicates a temporary issue with your server.

Timeout (>30s)No response

Event 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.

Async processing with 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.

Cresva CLI
# 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 10

Using ngrok

ngrok creates a public tunnel to your local server. Register the ngrok URL as your webhook endpoint in the Cresva dashboard.

ngrok
# 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/cresva

Dashboard 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.

javascript
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:

bash
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 via API
# 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:

Structured logging example
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

  1. Review the changelog for the new version at developers.cresva.ai/protocol/changelog.
  2. Create a second endpoint on the new version pointing to a staging URL.
  3. Send test events to validate your handler against the new payload shapes.
  4. Update your production handler to support both old and new formats.
  5. Upgrade the production endpoint version in the dashboard.
  6. 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.

Per endpoint1,000 deliveries/minuteSustained delivery rate per configured endpoint
Per account10,000 deliveries/minuteAggregate across all endpoints on your account
Burst200 deliveries/secondShort burst limit per endpoint for bursty events
Payload size256 KB maxEvents with larger payloads are truncated; full data available via API

When 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

Signature mismatch

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.

Events not arriving

Confirm the endpoint is enabled in the dashboard. Check that you subscribed to the correct event types. Verify HTTPS is valid (no expired certificates).

Duplicate events

Implement idempotency using the event id field. This is expected behavior; webhook delivery is at-least-once.

Endpoint auto-disabled

Your endpoint returned non-2xx for 6 consecutive attempts. Fix the issue, then re-enable from Settings → Webhooks.

Timeout errors

Return 200 immediately and process asynchronously. Your handler has 30 seconds before Cresva considers the delivery failed.

Payload truncated

Payloads exceeding 256 KB are truncated. Use the resource ID in the payload to fetch full data from the API.

Wrong payload version

Check your endpoint API version in Settings → Webhooks. Upgrade if you expect the latest payload schema.