> ## Documentation Index
> Fetch the complete documentation index at: https://docs.salesive.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Checkout functions

> Offer automatic discounts at checkout: register one signed callback per installation, answer with money off lines or the whole order, and let Salesive apply the caps.

## Overview

A checkout function is work your app does during checkout, after the cart has
been priced and before payment. Salesive calls your HTTPS endpoint while the
buyer waits, validates the answer, and applies what it can.

The **discount** kind is built. It returns money off individual lines or off the
order total, with no code from the buyer.

| Function | Status | Scope |
| - | - | - |
| **Discount** | Built | `WRITE_DISCOUNT_FUNCTIONS` |
| **Payment customization** | Designed, not live | None yet |
| **Delivery customization** | Designed, not live | None yet |

The transport is the same pattern as [shipping rates and cart
validation](https://docs.salesive.com/apps-api/checkout-extensions.md): a signed JSON request, a hard
deadline, an SSRF guard and strict response validation. A checkout never breaks
because an app is down.

## Register the discount callback

An installed app registers one callback per installation with its own `app_`
token:

| Method | Path | Scope |
| - | - | - |
| `PUT` | `/api/v1/checkout-extensions/discount` | `WRITE_DISCOUNT_FUNCTIONS` |
| `GET` | `/api/v1/checkout-extensions/discount` | `WRITE_DISCOUNT_FUNCTIONS` |
| `DELETE` | `/api/v1/checkout-extensions/discount` | `WRITE_DISCOUNT_FUNCTIONS` |

```bash theme={null}
curl -X PUT https://api.salesive.com/api/v1/checkout-extensions/discount \
  -H "Authorization: Bearer $APP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://discounts.example.com/salesive/checkout", "name": "Loyalty discount" }'
```

| Field | Type | Notes |
| - | - | - |
| `url` | string | Required. Public `https` URL on port 443. At most 2,048 characters |
| `name` | string | Optional. At most 60 characters. Used as the discount title when your answer has no title |
| `active` | boolean | Optional. `false` keeps the registration but stops the calls |

The registration is bound to the installation behind the token, never to
anything in the request. There is one row per installation, so a second `PUT`
replaces the first. `GET` returns `404` when nothing is registered. Uninstalling
the app switches the callback off.

The URL is checked with the webhook URL guard and then with the callback guard
Salesive applies before every call. Both are static plus DNS checks and neither
makes a request. A URL that registers is therefore one that can actually be
called: public, HTTPS, port 443, no credentials and no private or loopback
address.

## The request

Salesive sends a `POST` with `Content-Type: application/json` and the topic
`discount/request`:

```json theme={null}
{
  "id": "7f3c2a9e-1a2b-4c3d-8e9f-0a1b2c3d4e5f",
  "topic": "discount/request",
  "shopId": "64b1f0a3c2d4e5f6a7b8c9d0",
  "sentAt": "2026-11-07T10:00:00.000Z",
  "currency": "NGN",
  "shop": { "id": "64b1f0a3c2d4e5f6a7b8c9d0", "name": "Ada Shoes", "type": "ecommerce" },
  "cart": {
    "id": "64c1f0a3c2d4e5f6a7b8c9d1",
    "subtotal": 12500,
    "lines": [
      {
        "lineItemId": "64d1f0a3c2d4e5f6a7b8c9d2",
        "productId": "64e1f0a3c2d4e5f6a7b8c9d3",
        "variant": "64f1f0a3c2d4e5f6a7b8c9d4",
        "quantity": 2,
        "unitPrice": 5000,
        "subtotal": 10000
      }
    ]
  },
  "customer": { "id": "64a1f0a3c2d4e5f6a7b8c9d5", "guest": false }
}
```

* `id`, `topic`, `shopId` and `sentAt` are added by the platform. They are inside
  the signed body, so reject a replay by `id` or a stale request by `sentAt`.
* `lineItemId` is the stable handle to echo in a line level discount.
* `productId` is the catalog id of the line: a product, food, service or digital
  product. `variant` is the variant id, or `null`.
* `unitPrice` includes addons and is what the buyer pays per unit.
* `customer` is `null` when nobody is signed in. A guest account carries
  `guest: true`. No name, email, phone or address is sent.
* Amounts are JSON numbers in the shop currency, rounded to two decimals.

## The response

Answer with a `discounts` array:

```json theme={null}
{
  "discounts": [
    {
      "lineItemId": "64d1f0a3c2d4e5f6a7b8c9d2",
      "amount": 1000,
      "code": "SHOES10",
      "title": "10% off shoes"
    },
    { "amount": 500, "title": "Store welcome" }
  ]
}
```

| Field | Rules |
| - | - |
| `amount` | Required. A JSON number, finite, greater than zero and at most 1e9. It is absolute money, not a percentage. A string is refused |
| `lineItemId` | Optional. Present makes it a line level discount. Omit it or send `null` for an order level discount |
| `title` | Optional. Shown to the buyer and stored on the order. At most 80 characters. When empty, the registered `name` and then the app name are used |
| `code` | Optional. A label stored for audit and display, at most 64 characters. It does not need to match a store coupon |

At most **20** discounts are read from one app. Validation is entry by entry: a
malformed entry is dropped without dropping the rest. A `lineItemId` that is not
one of the request's lines is dropped and is **never widened** to an order level
discount, so a typo cannot turn a line discount into money off the whole order.

## How Salesive applies the discounts

1. **Line level first.** Discounts are applied in the order the apps were loaded
   (oldest registration first), then in the order they appear in the response.
   Each line has a remaining capacity equal to its subtotal. A discount takes the
   smaller of its `amount` and that capacity, so a line never goes negative and a
   second app cannot discount money the first already removed.
2. **Order level next.** These are applied in the same order, each capped at what
   is left of the cart subtotal.
3. **The total is capped at the subtotal.** A discount can never take the order
   total below zero.
4. **A grant reduced to zero is not recorded.**

The order stores the total in `order.discount`, so the order total, the platform
fee rule and every existing reader keep working. Each applied grant is also
stored in `order.discountApplications`:

```js theme={null}
{
  source: "app",
  app, installation, extension,
  title, code,
  level: "line" | "order",
  lineItemId,
  amount,
  requestedAmount
}
```

`discountApplications` is absent on an order from a store with no discount
functions. The platform fee is computed on the discounted subtotal, exactly as
it is when a coupon is applied. A coupon applied after the order is created
stacks on what is left after the app discounts and is capped at the subtotal.

## Timeouts and failures

* Each app has **2 seconds**, and all apps are called in parallel.
  `APP_DISCOUNT_TIMEOUT_MS` overrides the deadline, and the client clamps it to 5
  seconds. The deadline covers DNS, connect, headers and body.
* A timeout, a non 2xx answer, a redirect, a response over 64 KB, a body that is
  not a JSON object, a blocked URL, a missing signing secret or an internal error
  contributes no discount.
* The checkout never breaks. On any failure the resolved result is no discount,
  and order creation carries on. A store whose discount app is down still sells
  at the normal price.
* Failures are not shown to the buyer. They are logged with the app,
  installation, reason and duration.

The resolved result is cached per cart hash for **30 seconds**. The hash covers
the shop, cart id, currency, subtotal, customer id and guest flag, and every
line's id, product id, variant, quantity, unit price and subtotal. A cart edit is
a new key, and a retry of the same cart within the window does not call the apps
again. A failed app is cached as no discount for that window.

## Signing

Discount requests use the same construction as [webhook signature
v2](https://docs.salesive.com/apps-api/webhooks.md#verify-the-signature) and the [app proxy](https://docs.salesive.com/apps/app-proxy.md):

```http theme={null}
X-Salesive-Signature: t=<unix seconds>,v1=<base64 HMAC-SHA256 of "<t>.<raw body>">
```

The key is the app's webhook signing secret for the installation's environment:
the development secret for a development install when the app has one, otherwise
the production secret.

The older `X-Salesive-Hmac-SHA256` header, a base64 HMAC-SHA256 of the raw body,
is sent too, so an app that already verifies carrier rate and validation calls can
reuse that code. Both headers sign the same request body bytes.

| Header | Value |
| - | - |
| `X-Salesive-Signature` | `t=<unix seconds>,v1=<signature>` |
| `X-Salesive-Hmac-SHA256` | Signature of the raw body |
| `X-Salesive-Topic` | `discount/request` |
| `X-Salesive-Shop-Id` | The store id |
| `X-Salesive-App-Client-Id` | Your client id |
| `X-Salesive-Request-Id` | Same as `id` in the body |

Verify the raw bytes before parsing, and reject a timestamp more than **300
seconds** from your clock, in either direction. An app with no signing secret is
never called.

## Releases

When you create a release draft, Salesive records the checkout callbacks the app
had registered under `checkoutExtensions` in the release snapshot, one row per
installation and kind. Publishing, a staged rollout and rollback carry that
record with the version.

The snapshot is an audit and rollback record. Publishing does **not** rewrite the
live registrations: the checkout path still reads the installation's
`AppCheckoutExtension` row, so publishing a release can never move a store's
checkout to a URL the merchant did not register.

## The other kinds

Payment customization and delivery customization are designed and not live.

* **Payment customization** would run when the payment step is built, after the
  order exists. It would hide, rename or reorder payment methods, and add a short
  message next to one. It would never create a payment session or mark an order
  paid.
* **Delivery customization** would run at the shipping step. It would rename,
  reorder or hide the options the store already has, and add a short message. It
  would not change a price; a new price belongs to the shipping rates kind.

Both would use the same envelope and signing as a discount. The store's own
methods and options would stay unless the merchant enables the app, and the
platform would refuse to hide the last available one. Both need a merchant
switch and a product decision before a build.

## Next steps

> [card] [Checkout extensions](https://docs.salesive.com/apps-api/checkout-extensions.md)
>
> Shipping rates and cart validation, the other synchronous checkout calls.

> [card] [Webhooks](https://docs.salesive.com/apps-api/webhooks.md)
>
> Signature v2, retries and the full topic list.

> [card] [Releases](https://docs.salesive.com/apps/releases.md)
>
> How a release snapshot freezes delivery config.

> [card] [Scopes & permissions](https://docs.salesive.com/apps/scopes-permissions.md)
>
> What `WRITE_DISCOUNT_FUNCTIONS` grants and how the merchant sees it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.