Delivery rates
Offer your own delivery prices at checkout, beside the shop's.
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.
[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.
{
"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:
{
"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:
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.