Skip to content
cresvaDevelopers

Webhooks

Cresva defines 36 event types, 8 of which nothing publishes today. A delivery is a POST of a JSON envelope, signed with HMAC-SHA256, to a URL you register.

The envelope

Every delivery has the same outer shape. The event you subscribed to is nested under event, and the two timestamps are different: one is when the event happened, the other is when this delivery attempt was made.

JSON
{
  "event": {
    "id": "d1ae5039-4640-4c9f-9e49-e2ee545cde2f",
    "type": "storefront.search_empty",
    "data": { /* shape depends on type */ },
    "timestamp": "2026-09-16T10:04:11.201Z"
  },
  "webhook_id": "cmu0acdx100klxgqvb5iobs1m",
  "timestamp": "2026-09-16T10:04:11.884Z"
}

Headers

X-Cresva-SignaturestringHMAC-SHA256 of the raw request body, hex, prefixed with "sha256=".
X-Cresva-EventstringThe event type, so you can route without parsing the body.
X-Cresva-Delivery-IdstringUnique per delivery attempt. Use it to make your handler idempotent.
User-AgentstringCresva-Webhooks/1.0

If you built against this page before 2026-09-16

It documented X-ACP-Signature and the sender has never sent that header. It also showed a comparison against a bare hex digest, and the real value carries a sha256= prefix. If your verification has been failing, or if you disabled it to get deliveries working, both defects are corrected below.

Verifying a delivery

Compute HMAC-SHA256 over the raw request body, prefix it, and compare in constant time. Parsing the body to JSON and re-serialising it will change the bytes and the signature will not match.

text
import crypto from "node:crypto";

export function verify(rawBody, signatureHeader, secret) {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");

  const a = Buffer.from(signatureHeader ?? "", "utf8");
  const b = Buffer.from(expected, "utf8");
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

// In an Express handler, take the raw body before any JSON parser:
//   app.post("/webhooks/cresva", express.raw({ type: "application/json" }), (req, res) => {
//     const ok = verify(req.body, req.get("X-Cresva-Signature"), process.env.CRESVA_WEBHOOK_SECRET);
//     if (!ok) return res.status(401).send("bad signature");
//     res.sendStatus(200);
//   });

Retries

A delivery that does not get a 2xx is retried 5 times, at 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours after the first attempt. After the last one it stops.

Return 200 as soon as you have stored the delivery, and do your work afterwards. A handler that does its processing before replying will be retried while it is still working, and you will process the same event twice.

Deduplicate on X-Cresva-Delivery-Id. It is unique per attempt, so an event redelivered after a timeout carries a different one; if you need to deduplicate on the event itself, use event.id from the body.

Two things the sender will not do

It does not follow redirects. A 301 or 302 from your endpoint is a failed delivery, not a hop. Register the final URL.

It will not deliver to a private address. The URL is re-validated against public-address rules immediately before each request rather than only when you save it, because DNS can change in between. A signed payload leaves only after that check passes.

The 36 event types

Subscribe to the ones you need, or to * for all of them. An endpoint receives only the types it is subscribed to.

transaction.*

The order lifecycle, from created to completed or disputed.

transaction.createdA transaction record was opened.
transaction.confirmedThe transaction was confirmed.
transaction.paidnot yet emittedPayment was recorded against it. Cresva did not take it: this reflects what the merchant's own rails reported.
transaction.shippednot yet emittedThe merchant marked it shipped.
transaction.completedThe transaction reached its end state.
transaction.cancelledIt was cancelled before completion.
transaction.disputedA dispute was recorded. Cresva records it and does not adjudicate it.
transaction.refundednot yet emittedA refund was recorded.

confirmation_window.*

The status-only window that replaced escrow in OSP v3.0. It records whether a buyer confirmed or disputed. It holds nothing.

confirmation_window.openednot yet emittedThe window opened on a transaction.
confirmation_window.confirmedThe buyer confirmed and the window closed early.
confirmation_window.disputedThe buyer disputed within the window.

storefront.*

What agents did against the catalogue.

storefront.queryAn agent ran a structured query.
storefront.search_emptyA search returned nothing. This is the one worth subscribing to first: it is demand you could not answer.
storefront.product_viewednot yet emittedAn agent read one product.
storefront.product_recommendedA product was returned by the recommend endpoint.

offer.*

Offer claims and their exhaustion.

offer.claimedAn agent claimed an offer.
offer.redeemedA claimed offer was redeemed.
offer.depletedAn offer ran out of remaining claims.
offer.expiredAn offer passed its end date.

certification.*

Certification run lifecycle. Note that no certification has been issued in production yet, so these have never fired for a real brand.

certification.startedA certification run began.
certification.passedThe run reached bronze or above.
certification.failedThe run scored below 50.
certification.expiringA certificate is within 7 days of expiry.
certification.revokedA certificate was revoked.

alert.*

Conditions worth a merchant's attention.

alert.visibility_dropAgent visibility fell.
alert.competitor_gainedA competitor gained ground.
alert.zero_results_trendingnot yet emittedSearches returning nothing are trending up.
alert.rate_limit_hitCallers are hitting a rate limit against this brand.

funnel.*

Agent-originated funnel steps.

funnel.agent_clickAn agent followed a purchase URL.
funnel.purchaseA purchase was attributed to an agent path.
funnel.review_requestednot yet emittedA review was requested.

product.*

Changes to a catalogue entry.

product.updatedA product changed.
product.new_reviewnot yet emittedA review landed on a product.
product.score_changedA product's score moved.

feedback.*

Agent feedback on a storefront.

feedback.submittedFeedback was submitted.
feedback.negativeThe feedback was negative.

Registering an endpoint

Endpoints are registered from the Cresva dashboard, under the storefront settings for a brand. There is no public endpoint for managing them, and this page does not document one: an earlier version of it showed management cURL examples for routes that are not exposed.