Custom Storefront
This guide walks through integrating Deducto's promotion system with a custom ecommerce storefront. If you have built your own store (like a Progressive Web App) rather than using platforms like Shopify or WooCommerce, follow this guide to add Deducto's promotion capabilities. Since this involves custom development work, the guide is written with developers in mind.
Deducto evaluates promotions by analysing cart data sent from your storefront. Throughout the checkout flow, your storefront needs to communicate with Deducto's API endpoints by sending the customer's current cart information, allowing Deducto to determine which promotions should be applied.
Prerequisites
To integrate Deducto into your storefront, you'll need sufficient technical control and access to implement the following requirements:
- Your platform must allow modifying both backend APIs and frontend presentation code
- Your integration must be able to make HTTP requests to Deducto's cloud-hosted API endpoints
- You must be able to control the checkout flow to handle promotion-related rejections and errors when schedules or usage limits are reached
- You must be able to display the results of API calls to customers, including discounted totals, prices and applied promotions, either directly in the frontend or rendered from the backend
- You must be able to attach and persist promotion data throughout the checkout process, including storing the applied promotions and the customer's email alongside each order so the Unrecord step can clean them up later if the order is cancelled or refunded
You will also need the following before starting this guide:
- A Deducto account - check out the Setting up an Account guide if you don't have one yet
- A Deducto project - head over to Creating a Project if you need help setting this up
- An ecommerce storefront that is not powered by Shopify or WooCommerce (these do not require this custom storefront integration step)
How it works
Deducto replaces your cart totalling system with its promotion evaluation system. It provides endpoints that are called at various stages of the checkout process. Each endpoint receives information about the customer's cart and returns discounted totals and applied promotions. You can then apply the prices and discount descriptions on your storefront's cart and checkout pages.
There are 4 storefront API endpoints that are used to evaluate promotions throughout the checkout flow:
| Endpoint | Description |
|---|---|
| Verify | The verify endpoint is called every time a customer's cart is updated. It re-evaluates applicable promotions and returns updated subtotals, totals and shipping costs. |
| Verify Expected | The verify-expected endpoint is called when the user clicks the order placement button, but right before sending the order for processing. It compares the current promotions against those from the previous verify call to confirm nothing has changed since the customer viewed their cart. This ensures pricing consistency through checkout. |
| Record | The record endpoint is called when processing an order for payment. It runs the same checks as verify-expected, ensuring the list of applied promotions hasn't changed, and if successful, records the usage of promotions and coupons for the customer. Once it responds successfully, the final order price is considered valid and can proceed to further processing and checks. \n \nIf the record endpoint fails, the order must be rejected and the customer informed that promotions have changed. This prevents customers from being charged amounts different from what they saw at checkout. |
| Unrecord | The unrecord endpoint enables deletion of usage data for a particular order. If an order is cancelled or refunded, you can call the unrecord endpoint to delete that order's usage data which will allow the customer to make a purchase with the same promotion again. |
The diagram below shows when each endpoint is called during a typical cart-to-checkout flow:
sequenceDiagram
autonumber
actor Customer
participant Storefront as Your storefront / backend
participant Deducto
Note over Customer,Storefront: Cart updates
Customer->>Storefront: Add / remove / change cart item
Storefront->>Deducto: POST /v1/checkout/{projectId}/verify
Deducto-->>Storefront: appliedPromotions, rejectedPromotions, resolvedCart
Storefront-->>Customer: Show discounted totals
Note over Customer,Storefront: Checkout
Customer->>Storefront: Click "Place order"
Storefront->>Deducto: POST /v1/checkout/{projectId}/verify-expected
Deducto-->>Storefront: expectationsWereMet?
alt expectationsWereMet = true
Storefront->>Storefront: Capture payment
Storefront->>Deducto: POST /v1/checkout/{projectId}/record
Deducto-->>Storefront: Promotion usage recorded
Storefront-->>Customer: Order confirmation
else expectationsWereMet = false
Storefront-->>Customer: "Price has changed" notice
Note over Storefront: Re-call Verify to refresh
end
Note over Customer,Storefront: (Later) Refund or cancellation
opt Order cancelled or refunded
Storefront->>Deducto: POST /v1/checkout/{projectId}/unrecord
Deducto-->>Storefront: Usage records cleared
end
This separation of endpoints ensures pricing consistency throughout checkout while maintaining proper tracking of promotion usage limits and scheduling rules.
Getting API keys
To authenticate with the Deducto API, you'll need two types of API keys, which are project-specific:
-
Frontend API Key
- Used for Verify and Verify Expected endpoints
- Can be used directly from frontend code, but we strongly recommend calling these endpoints from your backend and proxying the response to your frontend. Reasons:
- Cost control – your backend can debounce, deduplicate, and validate cart data before calling Deducto. A frontend-only flow lets every refresh and every bot run up your API request count.
- Cart-tamper resistance – the backend can verify that the cart it sends to Deducto matches the prices and items in your database, instead of trusting whatever the frontend supplies.
- Easier debugging – you see exactly what Deducto returned for which cart, useful when a customer questions a price.
-
Backend API Key
- Used for Record and Unrecord endpoints
- Must be kept strictly confidential
- Controls promotion/coupon usage tracking
- Should only be used server-side during checkout processing
- If compromised, could be used to bypass usage limits of a promotion
To obtain your API keys:
- Open the Deducto Dashboard
- Navigate to your project
- Click on the "API Keys" tab
- Click the "Add Frontend API Key" or "Add Backend API Key" buttons if you don't already have them
- Copy the API key to your secure storage - they will not be shown again once you close the dialogue
The Project API URL can also be found on the API Keys page. This URL serves as the base endpoint for all API requests you'll make to the Deducto API for your specific project.
Setting up the Verify calls
The first step in linking with Deducto is getting the promotions applicable to a customer's cart. This is achieved through the Verify endpoint, which provides updated promotion calculations whenever a customer's cart changes.
Since this endpoint is metered, you should only call it when the cart contents or relevant customer details actually change. Calling Verify periodically (e.g. on a timer) is strongly discouraged – it multiplies your usage costs across every customer session for no real gain. The Verify Expected and Record calls at checkout already re-evaluate the cart against the current time, current coupon state, and remaining promotion usage, so there's no need to "freshen" results between cart updates: if a promotion has expired or been used up since the customer first viewed it, Verify Expected will catch that at checkout.
Understanding the request
An example request body is shown below:
{
"couponCodes": ["SUMMER2024"],
"cart": {
"currency": "USD",
"cartItems": [
...
],
"attributes": {
"abTest": "testGroup1" // Your custom cart attributes
},
"customer": { // Optional for Verify; required by default for Verify Expected and Record
"email": "[email protected]",
"countryId": "US"
},
"shipping": { // Optional
"methods": [
{
"methodCode": "standard",
"originalPrice": "10.00"
},
{
"methodCode": "express",
"originalPrice": "15.00"
}
]
}
}
}
The shipping field is optional. To support shipping discounts, provide Deducto with all shipping methods applicable to this cart and their costs. Deducto will determine discounts for all shipping methods provided, which your customer can then compare and select from. It is your storefront's responsibility to add the final selected shipping cost to the cart total.
The main component of the cart object is the cartItems field, which can look like this:
[
{
"productData": {
"productId": "...",
"name": "Product Name",
"originalPrice": "99.99", // Format: "0.00"
"salePrice": "79.99", // Can be null
"isOnSale": false, // Optional
"variantId": "...", // Can be null
"attributes": { // Your custom product attributes
"category": "shoes",
"color": "blue",
// etc...
}
},
"qty": 1
}
]
You'll need a way to convert your cart items into Deducto's expected format. This typically involves mapping basic product information (ID, name, price, etc.) and any custom attributes you've configured in your Deducto project. The exact implementation will depend on your product data structure.
The last building block is to assemble the set of attributes for the cart. Include any custom attributes you've configured in your Deducto project in the attributes field.
For the exact details of the request body, see the Verify Endpoint API reference.
TipSince the Verify, Verify Expected, and Record endpoints accept similar request body structures, we recommend making these processing steps reusable.
To make the request to the Verify endpoint, you'll need to send a POST request with the request body above. Storefront API endpoints require the API key to be sent in the Authorization header:
POST /v1/checkout/{projectId}/verify
Authorization: Bearer {your-frontend-api-key}
Content-Type: application/json
The corresponding paths for the other endpoints are /v1/checkout/{projectId}/verify-expected, /v1/checkout/{projectId}/record, and /v1/checkout/{projectId}/unrecord.
Understanding the response
The verify endpoint response contains several key sections that provide detailed information about how promotions should be applied to the cart:
promotions: Object containing all promotions that were considered for the cart, keyed by promotionId. An example may look like this:
{
"...": { // Key is the promotionId
"promotionId": "...", // UUID of the promotion
"versionId": "...", // UUID of the promotion version
"name": "...", // Display name of the promotion
"exclusive": false, // Whether this promotion is exclusive
"triggeringCouponCodes": ["..."] // Coupon codes that activated this promotion
}
}
appliedPromotions: Object containing the promotions that were successfully applied to the cart, keyed by promotionId. Only the promotion ID and promotion version ID are included. An example is shown below:
{
"...": { // Key is the promotionId
"promotionId": "...", // UUID of the promotion
"versionId": "..." // UUID of the promotion version
}
}
rejectedPromotions: Object containing promotions that were considered but couldn't be applied. An example is shown below:
{
"...": { // Key is the promotionId
"promotionId": "...", // UUID of the promotion
"versionId": "...", // UUID of the promotion version
"rejectionReason": "...", // Reason the promotion was rejected
... // Depending on the rejection reason, additional fields may be present to provide more context
}
}
NoteThe
rejectionReasonis a string value that can be any of the following:
PromotionUsageExceeded(response also includesusageCountLimit)PromotionPerCustomerUsageExceeded(response also includesusageCountLimit)ExclusivityStoppedAppliedPromotionsLimitReached(response also includesappliedPromotionsLimit)NoApplicableCartItemsDeclinedNot every rejection is surfaced – early-stage cases (currency mismatch, unmet trigger conditions, coupon-matching failures) are silently skipped rather than included in
rejectedPromotions. If you're troubleshooting why a promotion didn't apply and it isn't in this list, check the promotion's currency support, trigger conditions, and coupon configuration directly.
cartItemPromotions: Object that maps promotions to their discount effects on individual cart items, indexed by promotionId. Note that this only includes promotions applied directly to cart items - shipping promotions are not included. An example structure is shown below:
{
"...": { // Key is the promotionId
"promotionId": "...", // UUID of the promotion
"versionId": "...", // UUID of the promotion version
"discountAmount": "...", // Formatted as "0.00"
"discountAmountUnrounded": "...", // Pre-rounding discount amount
"discountAmountRoundingDelta": "..." // Difference from rounding
}
}
couponMatchResults: Object indexed by coupon code, describing what happened for each code the customer provided. Each entry tells you whether the code was valid, whether it was applied, and which promotions it triggered. An example for a valid, applied code:
{
"...": { // Key is the coupon code
"couponCode": "...",
"valid": true,
"applied": true,
"triggeredPromotions": {
"...": { // Key is the promotionId
"promotionId": "...",
"usageExceeded": false
}
}
}
}
If the code is not valid, the entry instead has "valid": false and an invalidReason field describing why (for example, UsageExceeded).
Note that this is not a discount totals map – for per-promotion discount amounts, use cartItemPromotions above.
resolvedCart: The final cart state after applying promotions. This is similar to the cart object supplied in the request body, but with calculated promotion results included. It also includes the shipping promotions that could be applied to the cart. An example is shown below:
{
"currency": "...",
"originalSubtotal": "...", // Pre-discount subtotal
"discountedSubtotal": "...", // Post-discount subtotal
"cartItems": [
{
"productId": "...",
"variantId": "...", // Optional
"originalSubtotal": "...",
"discountedSubtotal": "...",
"discountSteps": [
{
"promotionId": "...",
"versionId": "...",
"subtotalBefore": "15.00",
"discountAmount": "3.00",
"subtotalAfter": "12.00",
"subtotalAfterUnrounded": "12.00",
"subtotalAfterRoundingDelta": "0.00",
"extraParameters": {}
}
// ... other discount steps
]
}
],
"shipping": {
"methods": [
{
"methodCode": "standard",
"originalPrice": "15.00",
"discountedPrice": "12.00",
"bestDiscount": { // null if no shipping discount applied
"promotionId": "...",
"versionId": "...",
"subtotalBefore": "15.00",
"discountAmount": "3.00",
"subtotalAfter": "12.00",
"subtotalAfterUnrounded": "12.00",
"subtotalAfterRoundingDelta": "0.00",
"extraParameters": {}
}
}
]
},
"promotionalCartItems": [ // Any gift items will be included here
{
"promotionId": "...",
"versionId": "...",
"productId": "...",
"qty": 1
}
]
}
For the exact details of the response body, see the Verify Endpoint API reference.
Using the response
The appliedPromotions and rejectedPromotions objects can be used to get IDs of the promotions that applied. These IDs can be used to get the names of the promotions from the promotions object, or the total amount discounted by each promotion from the cartItemPromotions object.
You should maintain a record of the applied promotions list in your storefront since it will be needed when you call the Verify Expected and Record endpoints later in the checkout flow.
The resolvedCart object can be used by your storefront to display the final cart state to the customer, including the discounted prices for shipping methods. Your checkout system should also use figures from this object as the source of truth.
The shipping methods included in the resolvedCart.shipping.methods array should be used by your storefront to display the available shipping options and costs to the customer.
Any gift products included in resolvedCart.promotionalCartItems should be added to the cart by your storefront. We recommend marking these items as promotional in your cart and order data so your backend can distinguish them from paid items and handle them appropriately downstream.
If there are any errors obtaining the response, your storefront should handle the error case gracefully and display relevant information to the customer.
Congratulations! You've now completed the first and most important part of integrating Deducto in your custom storefront. The following endpoints, Verify Expected and Record, are very similar to the Verify endpoint you have just completed.
Adding the Verify Expected call and Record call
The results from the Verify endpoint are enough for displaying cart details to the customer in various stages of the shopping process. This includes the cart page or cart drawer, and even the checkout page. Immediately after the customer clicks the order placement button, but before sending the order off for payment capture, you should make the Verify Expected call.
A typical order placement flow should look like this:
- Customer clicks the order placement button
- Your storefront makes a Verify Expected call to Deducto with cart details similar to the Verify endpoint call, plus a list of expected promotions. This list should come from the most recent Verify response that the customer actually saw – i.e. the one whose discounts and totals were displayed to them on the cart or checkout page. That's what makes Verify Expected a meaningful check: it confirms the discounts you're about to charge match what the customer agreed to.
- Deducto calculates the promotions again and compares them to the expected list
- If there is a mismatch, a
expectationsWereMetoffalseis returned. Your storefront should then inform the user that something has changed and the originally quoted price is no longer valid (perhaps a promotion has expired) - If the expectations were met, continue with the checkout process
- Capture the payment, or do any other processing required to complete the order
- Call Deducto's Record endpoint to record the promotion usage. The request body is the same as the one used for Verify Expected.
Customer details (email at minimum) are required on both Verify Expected and Record requests, unlike Verify where the customer field is optional. If you need to support anonymous orders (no customer attached), set allowAnonymousSales: true in the request body – otherwise the call will be rejected with a missing-customer error.
The Verify Expected call acts as a dry run of the Record endpoint without committing any usage records. This allows Deducto to validate that the cart will still receive the same promotions, given the current timing, coupon codes, and remaining promotion uses. It prevents issues where conditions may have changed between cart modification and order placement. This gives you a final opportunity to check with Deducto before charging the customer, avoiding potentially complex resolution steps if the price was incorrect.
Your storefront should gracefully handle any errors from the Verify Expected endpoint, informing the customer that something has changed and the originally quoted price is no longer valid (perhaps a promotion has expired), and call Verify again to re-evaluate the cart.
The Record endpoint behaves differently from Verify Expected on a mismatch: instead of returning a success response with expectationsWereMet: false, it returns an error response. Treat any error response from Record as a failed recording – most commonly an expectations mismatch that slipped through between Verify Expected and Record, but could also be a transient failure. Your storefront should not consider the order's promotion usage recorded, and should surface the issue (typically by re-running Verify and showing the updated totals to the customer, plus a "price has changed" notice).
A successful Record response means the promotions were applied as expected and the usage of any coupon codes and customer email have been recorded. You should consider persisting the applied promotion details and customer details in your system at this point – they'll be needed if you ever want to call Unrecord to clean up the usage records after a cancellation or refund.
For the exact details of the request and response bodies, see the Verify Expected and Record API references.
Unrecord
The Unrecord step allows you to undo a previously Recorded usage. This is particularly useful when you need to cancel or refund an order and want to clean up the associated promotion usage records. Note that you may not need this step if you don't offer order cancellation functionality or if your promotions don't have usage limits.
To unrecord, send the Unrecord endpoint an appliedPromotions object built from the original Record response. Each entry in the request must include the promotion's promotionId, versionId, and the triggeringCouponCodes (which you can take from the matching entry in the Record response's promotions object). You also pass the customer details (email, and optionally countryId).
Once unrecorded, Deducto treats those promotions and coupons as if they were never used by that customer, allowing the customer to use them again if the promotion has limits on usage.
For the exact details of the request and response bodies, see the Unrecord API reference.
Updated 9 days ago