# Delivery rates

> Offer your own delivery prices at checkout, beside the shop's.

Source: https://storeware.ai/docs/extensions/delivery-rates

An app can offer delivery choices at checkout: a local courier, a pallet service, a price worked out from your own rules. Storeware asks your app for its rates whenever a basket has a delivery address, and shows them beside the shop's own. A shopper chooses from both lists as one.

```toml
[oauth]
scopes = ["checkout:read"]

[checkout.delivery_rates]
url = "https://courier.example.com/storeware/rates"
```

`checkout:read` is required, because the request carries the shopper's address and basket. The merchant sees that on the consent screen. If a merchant later takes the scope away, your app is no longer asked.

## What you are asked

A signed `POST` with the basket and where it is going. Every amount is in the basket currency's minor unit, such as cents or pence.

```json
{
  "storeId": "…",
  "currency": "GBP",
  "destination": { "country": "GB", "region": null, "postalCode": "LS1 1AA", "city": "Leeds" },
  "lines": [
    {
      "variantId": "…",
      "sku": "MUG-1",
      "title": "Mug",
      "quantity": 2,
      "unitPrice": 1200,
      "grams": 400
    }
  ],
  "subtotal": 2400
}
```

Only lines that are delivered are sent, so a download is left out. `grams` is per unit, and `null` when the merchant has not weighed the product. `subtotal` is the goods after any discount on them, before delivery and tax.

You are not asked for a basket with no address, a basket of downloads only, or a subscription renewal. An answer is reused for up to five minutes while the request stays the same, so a shopper typing in the delivery form does not ask you on every change. Expect to be asked the same question now and then all the same, and answer it the same way.

## What you answer

Within 3 seconds, at most 32 KB, a list of up to 20 rates:

```json
{
  "rates": [
    {
      "id": "next_day",
      "name": "Next day",
      "amount": 695,
      "description": "Before noon",
      "min_days": 1,
      "max_days": 1
    },
    { "id": "economy", "name": "Economy", "amount": 295, "max_days": 5 }
  ]
}
```

| Field         | Rules                                                               |
| ------------- | ------------------------------------------------------------------- |
| `id`          | Letters, digits, `_` and `-`, up to 64. Yours, and stable.          |
| `name`        | What the shopper reads, up to 80 characters.                        |
| `amount`      | A whole number of minor units, zero or more. Zero is free delivery. |
| `description` | Optional, up to 200 characters.                                     |
| `min_days`    | Optional. Days until it arrives at the soonest.                     |
| `max_days`    | Optional. Days until it arrives at the latest.                      |

An empty list is a good answer: it means you do not deliver this basket. Keep each `id` the same for the same service. A shopper's choice is kept by it while they change their basket, as long as you go on offering it.

The SDK verifies the request and gives you the answer's schema:

```ts
import { deliveryRatesAnswer, verifyRatesRequest } from '@storeware/plugin-sdk/rates';

export async function POST(request: Request) {
  const basket = await verifyRatesRequest({
    secret: installationSecret,
    rawBody: await request.text(),
    signatureHeader: request.headers.get('storeware-signature'),
  });

  const answer = deliveryRatesAnswer.parse({ rates: await quote(basket) });
  return Response.json(answer);
}
```

The request is signed with your installation's secret as the kind `rates`, so nothing signed for another of your endpoints can be replayed here.

## When something goes wrong

Checkout never waits on you for long and never breaks because of you. If you take longer than 3 seconds, answer with anything but `2xx`, send more than 32 KB, or send something that does not match the schema, such as a negative amount or a fraction of a penny, the shopper sees the shop's own rates and nothing else, and the failure is logged. A second rate with an `id` you already used is dropped. Up to three apps are asked at once, so the wait is the slowest of them, never their sum.

Your rates reach every way a shopper or an AI agent checks out: the shop front, and agents buying through the Agentic Commerce Protocol and the Universal Commerce Protocol all see the same list.
