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:

EndpointDescription
VerifyThe 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 ExpectedThe 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.
RecordThe 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.
UnrecordThe 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:

  1. 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.
  2. 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:

  1. Open the Deducto Dashboard
  2. Navigate to your project
  3. Click on the "API Keys" tab
  4. Click the "Add Frontend API Key" or "Add Backend API Key" buttons if you don't already have them
  5. 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.

📘

Tip

Since 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
  }
}
📘

Note

The rejectionReason is a string value that can be any of the following:

  • PromotionUsageExceeded (response also includes usageCountLimit)
  • PromotionPerCustomerUsageExceeded (response also includes usageCountLimit)
  • Exclusivity
  • Stopped
  • AppliedPromotionsLimitReached (response also includes appliedPromotionsLimit)
  • NoApplicableCartItems
  • Declined

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

  1. Customer clicks the order placement button
  2. 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.
  3. Deducto calculates the promotions again and compares them to the expected list
  4. If there is a mismatch, a expectationsWereMet of false is 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)
  5. If the expectations were met, continue with the checkout process
  6. Capture the payment, or do any other processing required to complete the order
  7. 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.