Shipping prices in the cart, without shipping the credentials
Buyers on creativeautomation.ae couldn't see delivery cost until they were inside checkout. Fixing that meant calling a carrier aggregator from a Shopify theme — exactly the thing you cannot safely do.
The problem
OTO aggregates hundreds of delivery providers across the Gulf. Its delivery-fee endpoint authenticates with a long-lived refresh token exchanged for a short-lived access token. A Shopify theme is public JavaScript — anything the cart page can read, a customer can read — so calling OTO from the storefront would have published the account's credentials.
The alternative was what the store was already doing: leave shipping until checkout, and lose the carts that were price-sensitive about it.
What I built
A small Express service on Railway between the storefront and OTO, exposing one public endpoint: POST /api/shipping-estimate, taking a destination city, a chargeable weight and an optional order total. It returns a flat list of carrier options with price, ETA and logo.
Everything sensitive stays server-side. The refresh token lives only in Railway's environment — never in the repo, a commit, or a log line. The service exchanges it for a one-hour access token, caches it in memory, refreshes proactively around the 55-minute mark, and retries once if OTO returns a 401 or 403 mid-request.
The parts that matter in production
- CORS allow-list so only the store's own domains can call the endpoint.
- Two layers of rate limiting — 30 estimate requests a minute, plus a 120/min baseline across all routes, health check exempt.
- Short response cache, because ten shoppers in Riyadh with a two-kilo cart should not be ten calls to OTO.
- Timeouts and bounded retries upstream, so a slow carrier API degrades into a missing estimate rather than a hanging cart.
- Weight ceiling validated before the upstream call.
- Mock mode returning canned carrier data with no token, so the widget can be built locally without touching production credentials.
The cart side
The widget renders from the cart footer section. It reads /cart.js, converts line-item grams into a chargeable weight in kilograms, and posts that with the selected city. Carrier rows render with price, estimated window and — deliberately — the cash-on-delivery surcharge as a separate note rather than folded into the headline number.
Where it stops
The estimate is display-only and labelled that way. Shopify still calculates and charges its own rate at checkout; this shows an indicative cost early, at the moment it decides whether the cart survives.