> ## 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 extensions

> Take part in a store's checkout: quote your own shipping rates, or stop an order that breaks your rules.

## Overview

Checkout extensions let your app take part in checkout on a store that installed it.
Salesive calls your server while the buyer is checking out, and uses your answer straight away.

| Extension | What it does | Scope |
| - | - | - |
| **Shipping rates** | Your rates appear next to the store's own shipping options, and the buyer can pick one | `WRITE_SHIPPING_RATES` |
| **Cart validation** | When the buyer places the order, you can stop it with a message | `WRITE_CHECKOUT_VALIDATION` |

> [!NOTE]
>
> A buyer is waiting on every call. Answer within **3 seconds**. If you do not, or your answer
> is not valid, Salesive carries on without you: the store's own shipping options are shown,
> and the order goes through. Your app can never break a store's checkout.

## Register a callback

Register one callback URL per extension, per store, with your app token. The registration is
tied to the installation behind the token, so it only ever applies to that store.

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

| Method | Path | Scope |
| - | - | - |
| `PUT` | `/checkout-extensions/shipping-rates` | `WRITE_SHIPPING_RATES` |
| `GET` | `/checkout-extensions/shipping-rates` | `WRITE_SHIPPING_RATES` |
| `DELETE` | `/checkout-extensions/shipping-rates` | `WRITE_SHIPPING_RATES` |
| `PUT` | `/checkout-extensions/cart-validation` | `WRITE_CHECKOUT_VALIDATION` |
| `GET` | `/checkout-extensions/cart-validation` | `WRITE_CHECKOUT_VALIDATION` |
| `DELETE` | `/checkout-extensions/cart-validation` | `WRITE_CHECKOUT_VALIDATION` |

Body of a `PUT`:

| Field | Type | Notes |
| - | - | - |
| `url` | string | Required. Public `https` URL on the default port (443). |
| `name` | string | Shipping rates only. The heading buyers see over your rates (60 characters). Defaults to your app's name. |
| `active` | boolean | Optional. `false` keeps the registration but stops the calls. |

A `PUT` again replaces the registration. Uninstalling the app switches it off; after a
reinstall, register again.

**The URL must be public.** Salesive refuses plain `http`, other ports, URLs with a username or
password, and any host that resolves to a private, loopback or internal address. The check is
made when you register and again before every call, and redirects are never followed. For local
development, use a public https tunnel.

**Store types.** Shipping rates can only be registered on stores that ship: ecommerce stores
(products) and restaurants (food). Business stores (services) and digital stores get `409` with
code `store_does_not_ship`. Cart validation works on every store type.

## How calls are signed

Every call is a `POST` with a JSON body, signed exactly like [webhooks](https://docs.salesive.com/apps-api/webhooks.md#verify-the-signature):
the base64 HMAC-SHA256 of the raw body, keyed with your app's signing secret, in the
`X-Salesive-Hmac-SHA256` header. Development installs are signed with your development signing
secret when your app has one.

| Header | Value |
| - | - |
| `X-Salesive-Hmac-SHA256` | Signature of the raw body |
| `X-Salesive-Topic` | `shipping_rates/request` or `cart_validation/request` |
| `X-Salesive-Shop-Id` | The store's id |
| `X-Salesive-App-Client-Id` | Your client id |
| `X-Salesive-Request-Id` | Same as `id` in the body |

Every body has `id`, `topic`, `shopId` and `sentAt` (ISO time). They are covered by the
signature, so refuse a request whose `sentAt` is more than a few minutes old.

Buyer contact details are never sent: no name, phone number or email address.

## Shipping rates

Salesive calls you when the buyer reaches the shipping step, for orders that have items to ship
(products or food; services and digital items are left out).

```json Request theme={null}
{
  "id": "6f1c0e7e-1a1b-4bd0-9c55-0b5f6a2b8d10",
  "topic": "shipping_rates/request",
  "shopId": "64f1c2d3e4a5b6c7d8e9f001",
  "sentAt": "2026-11-05T10:15:00.000Z",
  "currency": "NGN",
  "shop": { "id": "64f1c2d3e4a5b6c7d8e9f001", "name": "Ada's Shoes", "type": "ecommerce" },
  "origin": { "address": "12 Allen Avenue", "city": "Ikeja", "state": "Lagos", "zipCode": null, "country": "Nigeria", "latitude": 6.6, "longitude": 3.35 },
  "destination": { "address": "4 Aminu Kano Crescent", "city": "Abuja", "state": "FCT", "zipCode": "900211", "country": "Nigeria", "latitude": null, "longitude": null },
  "order": {
    "id": "6720a1b2c3d4e5f6a7b8c9d0",
    "subtotal": 50000,
    "lines": [
      { "lineId": "6720a1b2c3d4e5f6a7b8c9d1", "itemType": "product", "itemId": "65aa...", "variantId": null,
        "name": "Leather sandal", "sku": "SND-42", "quantity": 2, "unitPrice": 25000, "weightKg": 0.8 }
    ]
  }
}
```

Answer with your rates:

```json Response theme={null}
{
  "rates": [
    { "id": "standard", "name": "Standard delivery", "price": 3500, "currency": "NGN", "minDeliveryDays": 2, "maxDeliveryDays": 4 },
    { "id": "express", "name": "Next day", "price": 7000, "currency": "NGN", "description": "Order before 2pm" }
  ]
}
```

| Field | Rules |
| - | - |
| `id` | Required. Letters, digits, `_ . : -`, up to 64 characters, unique in the answer. |
| `name` | Required. Up to 80 characters. |
| `price` | Required. A JSON number (not a string), 0 or more, in the store currency. Rounded to 2 decimal places. |
| `currency` | Required. Must be the request's `currency`. Rates in any other currency are dropped, never converted. |
| `description` | Optional. Up to 200 characters. |
| `minDeliveryDays`, `maxDeliveryDays` | Optional whole numbers from 0 to 365. |

Up to 10 rates are used. A rate that breaks a rule is dropped on its own; the others still
show. An empty `rates` list means you have nothing for this order.

**What the buyer sees.** Your rates appear as one shipping option, under your `name`, after the
store's own options. The price the buyer pays is the price from your answer, stored by Salesive;
nothing the buyer's browser sends can change it. Answers are reused for up to 10 minutes for
the same order and address, and you are called again after that or after you re-register.

**After the sale.** The order's shipment records which rate was chosen
(`metadata.rateId`, `metadata.rateName`). Use [webhooks](https://docs.salesive.com/apps-api/webhooks.md) and the Orders API
to pick the order up and fulfil it.

## Cart validation

Salesive calls you when the buyer places the order, before anything is created.

```json Request theme={null}
{
  "id": "0b8e4c1a-2f3d-4e5f-8a9b-0c1d2e3f4a5b",
  "topic": "cart_validation/request",
  "shopId": "64f1c2d3e4a5b6c7d8e9f001",
  "sentAt": "2026-11-05T10:14:00.000Z",
  "currency": "NGN",
  "shop": { "id": "64f1c2d3e4a5b6c7d8e9f001", "name": "Ada's Shoes", "type": "ecommerce" },
  "cart": {
    "id": "6720a0f1e2d3c4b5a6978877",
    "subtotal": 50000,
    "lines": [
      { "lineId": "6720a0f1e2d3c4b5a6978878", "itemType": "product", "itemId": "65aa...", "variantId": null,
        "name": "Leather sandal", "sku": "SND-42", "quantity": 2, "unitPrice": 25000 }
    ]
  },
  "customer": { "id": "65bb...", "guest": false }
}
```

To let the order through, answer `{ "errors": [] }`. To stop it, list what is wrong:

```json Response theme={null}
{
  "errors": [
    { "message": "You can buy at most 1 pair of this sandal per order.", "lineId": "6720a0f1e2d3c4b5a6978878" }
  ]
}
```

Each error is a string or an object with `message` (up to 300 characters) and an optional
`lineId`. Up to 5 messages are used. The buyer sees your messages and nothing is created: their
cart stays as it was. The storefront gets `422` with code `CHECKOUT_BLOCKED`.

> [!WARNING]
>
> Cart validation **fails open**. If you time out, return an error status, or answer with
> something that is not valid, the order goes through. Do not rely on it for checks that must
> never be skipped.

## Example handler

```js Node.js (Express) theme={null}
import express from "express";
import crypto from "crypto";

const app = express();
const SECRET = process.env.SALESIVE_WEBHOOK_SECRET; // whsec_...

function verified(req) {
  const signature = req.get("X-Salesive-Hmac-SHA256") || "";
  const expected = crypto.createHmac("sha256", SECRET).update(req.body).digest("base64");
  if (signature.length !== expected.length) return null;
  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) return null;
  const body = JSON.parse(req.body.toString("utf8"));
  if (Date.now() - Date.parse(body.sentAt) > 5 * 60 * 1000) return null; // replay
  return body;
}

app.post("/salesive/rates", express.raw({ type: "application/json" }), (req, res) => {
  const body = verified(req);
  if (!body) return res.status(401).end();
  const kg = body.order.lines.reduce((s, l) => s + (l.weightKg || 0) * l.quantity, 0);
  res.json({
    rates: [{ id: "standard", name: "Standard delivery", price: 2000 + Math.ceil(kg) * 500, currency: body.currency }],
  });
});
```

## Permissions

Both scopes are requested like any other on the OAuth `scope` parameter, and the merchant sees
them on the consent screen. See [Scopes & permissions](https://docs.salesive.com/apps/scopes-permissions.md).


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