Skip to main content

Checkout Hook

The Checkout Hook is a mechanism that allows you to inject custom logic into the checkout process before the order is finalized and the Stripe session is created.

When a customer starts a checkout, Webround collects all the necessary data—products, prices, taxes, shipping, customer data—signs it, and sends it to your endpoint. You can modify line items and order items, add custom metadata, and respond. Webround validates your response and proceeds with your data.


The Vision

The Checkout Hook is designed for use cases where pricing logic or order composition depends on data only you know: B2B contracts, custom price lists per customer, discounts based on company attributes, or integrations with external ERP or CRM systems.

Webround does not expose endpoints to send data to in advance. Instead, Webround contacts you at the right moment in the checkout flow. You respond with the modified data, and Webround uses it to create the order and the payment session.

This means you don't have to install anything on Webround. You only need to expose a publicly reachable HTTPS endpoint. You can use any technology and host it wherever you prefer.


Configuration

The Checkout Hook is configured in the dedicated section of the Webround Commerce admin panel.

ParameterDescription
URLThe HTTPS endpoint Webround will call during checkout
SecretKey generated by Webround to sign requests—shown only once
TimeoutMaximum wait time in ms (default 5000)
On Errorpassthrough — proceeds without changes on error; abort — blocks checkout
Secret

The secret is generated by Webround at creation time and shown only once. Store it securely. Webround never shows it again. Do not share it with anyone. If you need support, contact us.


Flow

Customer starts checkout

Webround validates cart, calculates prices, taxes, shipping, promotions

Webround signs the payload and calls your endpoint

Your endpoint validates the signature, processes, responds with signature

Webround validates your response

Webround creates order and payment session with your data

If your endpoint does not respond within the timeout or returns an error:

  • With onError: passthrough → Webround proceeds with original data, no additional logic is applied
  • With onError: abort → the checkout is blocked with a 500 error

Request Format

Webround sends a POST request to your endpoint with Content-Type: application/json.

The body contains:

{
"version": 1,
"storeId": "store-uuid",
"timestamp": 1745000000000,
"signature": "hmac-sha256...",
"store": {
"id": "store-uuid",
"plan": "start",
"countryCode": "IT",
"langCode": "it",
"defaultLangCode": "it"
},
"customer": {
"id": "customer-uuid",
"email": "[email protected]",
"isAuthenticated": true,
"emailVerified": true,
"isVatExempt": false,
"vatCode": "IT04917010615",
"displayName": "Mario Rossi",
"phone": null,
"newsletter": true,
"lifetimeValue": { "EUR": { "totalSpend": 1500.00, "totalRefund": 0 } },
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
},
"addresses": {
"shipping": { "fullName": "Mario Rossi", "countryCode": "IT", "addressLine1": "Via Roma 1", "city": "Rome", "postalCode": "00100", "province": "RM"},
"billing": { "fullName": "Mario Rossi", "countryCode": "IT", "addressLine1": "Via Roma 1", "city": "Rome", "postalCode": "00100", "province": "RM" }
},
"requestsInvoice": false,
"deliveryMethods": { "variant-uuid": "shipping" },
"selectedShippingMethodId": "shipping-method-uuid",
"promotionCodes": [],
"promotionSnapshot": [],
"items": [
{
"variantId": "uuid",
"productId": "uuid",
"priceId": "uuid",
"skuCode": "SKU001",
"quantity": 2,
"currencyCode": "EUR",
"deliveryMethod": "shipping",
"unitNet": 100.00,
"unitTax": 22.00,
"unitGross": 122.00,
"totalNet": 200.00,
"totalTax": 44.00,
"totalGross": 244.00,
"appliedTaxRate": 22,
"taxBehavior": "useWrTax",
"coverUrl": "https://...",
"optionsSnapshot": [],
"stripeProductId": null,
"stripePriceId": null,
"stripeTaxCodeId": null,
"totalStock": 50,
"cadence": "once"
}
],
"shippingOptions": [...],
"lineItems": [...]
}

Security — Validating Inbound Signature

Every Webround request includes an HMAC-SHA256 signature in the body as the signature field.

The signature is calculated on the entire body excluding the signature field itself:

bodyHash = SHA256(JSON.stringify(body without signature))
sigPayload = "{version}.{storeId}.{timestamp}.{bodyHash}"
signature = HMAC-SHA256(secret, sigPayload)

You must always validate the signature before processing the request.

You must also validate the timestamp: if the difference between the timestamp in the request and the current time exceeds 30 seconds, the request must be rejected. This neutralizes replay attacks—an attacker intercepting a legitimate request cannot re-send it after the window has expired.

function verifyIncoming(body, secret) {
const { signature, ...rest } = body;
const bodyHash = sha256(JSON.stringify(rest));
const sigPayload = `${body.version}.${body.storeId}.${body.timestamp}.${bodyHash}`;
const expected = hmacSha256(secret, sigPayload);
// use timing-safe comparison to prevent timing attacks
return timingSafeEqual(expected, signature);
}

const isValid = verifyIncoming(body, secret);
if (!isValid) return Response(401);

const age = Math.abs(Date.now() - body.timestamp);
if (age > 30_000) return Response(401);

Response Format

Webround expects a response with Content-Type: application/json and status 200.

The body must be exactly:

{
"version": 1,
"storeId": "store-uuid",
"timestamp": 1745000000000,
"signature": "hmac-sha256...",
"orderItems": [...],
"lineItems": [...],
"additionalData": {}
}
FieldTypeDescription
versionnumberMust be 1
storeIdstringThe same storeId received
timestampnumberUnix timestamp in milliseconds used for signing
signaturestringHMAC-SHA256 signature of your response
orderItemsarrayAt least 1 item — see schema below
lineItemsarrayAt least 1 item — Stripe-compatible line items
additionalDataobjectOptional — custom metadata saved to the order

orderItem Schema

{
"variantId": "uuid",
"productId": "uuid",
"priceId": "uuid",
"skuCode": "SKU001",
"quantity": 2,
"unitNet": 90.00,
"unitTax": 19.80,
"unitGross": 109.80,
"totalNet": 180.00,
"totalTax": 39.60,
"totalGross": 219.60,
"appliedTaxRate": 22,
"taxBehavior": "useWrTax",
"deliveryMethod": "shipping",
"coverUrl": null,
"optionsSnapshot": [],
"stripeProductId": null,
"stripePriceId": null,
"stripeTaxCodeId": null
}
Validation Constraints
  • variantId must belong to your store's catalog — unrecognized variantIds cause validation_failed
  • All amounts in lineItems must be >= 0 — negative amounts cause validation_failed
  • orderItems and lineItems must contain at least 1 element

Security — Signing the Response

Webround validates your response using the same logic you use to validate its request.

You must sign your response body excluding the signature field, using your timestamp (you can reuse the one received or generate a new one):

bodyHash = SHA256(JSON.stringify(response without signature))
sigPayload = "{version}.{storeId}.{timestamp}.{bodyHash}"
signature = HMAC-SHA256(secret, sigPayload)

If the signature is invalid, or if your timestamp is outside the 30-second window, Webround will treat the response as failed and apply the behavior configured in onError.


Implementation Example

const HOOK_VERSION = 1;
const TOLERANCE_MS = 30_000;

export default {
async fetch(request, env) {
if (request.method !== 'POST') return new Response('Method Not Allowed', { status: 405 });

const body = await request.json();
const secret = env.CHECKOUT_HOOK_SECRET;

// 1. Validate inbound signature
if (!verifySignature(body, secret)) {
return new Response('Unauthorized', { status: 401 });
}

// 2. Validate timestamp
if (Math.abs(Date.now() - body.timestamp) > TOLERANCE_MS) {
return new Response('Unauthorized', { status: 401 });
}

const { storeId, timestamp, items, lineItems } = body;
const customer = body.customer;

// 3. Your custom logic
// Example: 10% discount for customers with a VAT code
if (!customer?.vatCode) {
return respond(body, items, lineItems, secret);
}

const discountedItems = items.map(item => applyDiscount(item, 0.10));
const discountedLineItems = lineItems.map(li => ({
...li,
price_data: {
...li.price_data,
unit_amount: Math.round(li.price_data.unit_amount * 0.90),
}
}));

return respond(body, discountedItems, discountedLineItems, secret, {
vatDiscount: true,
discountApplied: '10%'
});
}
};

function respond(incomingBody, orderItems, lineItems, secret, additionalData) {
const { storeId, timestamp } = incomingBody;
const payload = {
version: HOOK_VERSION,
storeId,
timestamp,
orderItems,
lineItems,
...(additionalData ? { additionalData } : {}),
};
const signature = buildSignature(payload, secret, storeId, timestamp);
return new Response(JSON.stringify({ ...payload, signature }), {
headers: { 'Content-Type': 'application/json' },
});
}

function verifySignature(payload, secret) {
const { signature, ...rest } = payload;
const bodyHash = sha256(JSON.stringify(rest));
const sigPayload = `${HOOK_VERSION}.${payload.storeId}.${payload.timestamp}.${bodyHash}`;
const expected = hmacSha256(secret, sigPayload);
return timingSafeEqual(expected, signature);
}

function buildSignature(payload, secret, storeId, timestamp) {
const { signature: _, ...rest } = payload;
const bodyHash = sha256(JSON.stringify(rest));
const sigPayload = `${HOOK_VERSION}.${storeId}.${timestamp}.${bodyHash}`;
return hmacSha256(secret, sigPayload);
}

Logs and Monitoring

Every execution of the Checkout Hook is recorded. You can consult the logs in the Checkout Hook section of your Webround Commerce admin panel.

For each call, the following are available:

FieldDescription
Statusok, error, timeout, validation_failed
DurationResponse time in milliseconds
Fallback AppliedWhether Webround used original data instead of yours
Request PayloadThe full payload sent to your endpoint
Response PayloadThe response received from your endpoint
ErrorError message in case of failure

Error Scenarios

ScenarioBehavior
Endpoint unreachableerroronError
Timeout exceededtimeoutonError
HTTP non-200erroronError
Invalid response signaturevalidation_failedonError
Expired response timestampvalidation_failedonError
Invalid response schemavalidation_failedonError
Unknown variantIdvalidation_failedonError
Negative amount in lineItemvalidation_failedonError

With onError: passthrough, in all above cases, the checkout proceeds with the original data calculated by Webround. With onError: abort, the checkout is blocked.

Final Considerations

  • Technical Freedom: The endpoint can be implemented with any stack (Node.js, Go, Python, etc.) and hosted on any infrastructure. The only requirement is compliance with HTTPS protocol, JSON structure, and response times.
  • Single Source of Truth: Only one hook is allowed per store. This ensures that any data modifications come from a single endpoint, avoiding conflicts between competing pricing logics.
  • External Logic: The system is designed to integrate data residing in CRMs, ERPs, or proprietary databases. Webround delegates the final decision on price and order composition to your system, acting only as the executor towards Stripe Checkout.
  • Failure Management: Choosing between passthrough and abort allows for a balance between service continuity and custom logic precision.
  • End-to-End Security: Bidirectional signing ensures payload integrity for both inbound and outbound traffic, and timestamp validation prevents replay attacks.
Timeout Logic

Checkout is the most critical element of an e-commerce system. Drastically slowing down the system's response at this stage can lead to site abandonment and, consequently, order loss. We recommend keeping a timeout wide enough to allow for response delays, but also implementing your hook with technologies that respond quickly.

If your system suffers from cold starts, we suggest keeping it warm or using a system that does not have these issues, such as Cloudflare Workers, which balance serverless flexibility with Edge Computing responsiveness.