Car Battery Pro
Technical Reference

The backend API

The custom CBP API that powers vehicle lookup, postcodes, address search, and installation booking.

The backend API

🛠 Dev

The custom features are powered by a separate backend service - the eb-cbp-rego-lookup repository - deployed to Cloudflare Workers. The theme only ever talks to this API; the API in turn talks to the external providers (vehicle data, postcodes, Google services, Shopify Admin). It is maintained by Conversion Kings while CBP is on retainer (see Who owns what).

The backend has its own documentation in the eb-cbp-rego-lookup repo: a README.md, a Bruno/ Postman request collection under api-docs/, and the upstream AutoInfo specs under api-reference/. Use those for exact request/response payloads; this page is the operational overview.

Stack

ConcernTechnology
FrameworkHono on Cloudflare Workers
LanguageTypeScript
DatabaseCloudflare D1 (SQLite) via Drizzle ORM
Key/valueCloudflare KV (OAuth state + per-shop Shopify tokens)
AuthRS256 JWT via jose
Scheduled jobsCloudflare Cron Trigger (hourly)

Configuration in the theme

The base URL is the api_base_url theme setting, exposed to JS via snippets/api-init.liquid as window.theme.api.base (with a getEndpoint() helper). When debug mode is on, the api_dev_base_url (dev/staging) is used instead.

Authentication

All customer-facing endpoints require a Bearer JWT from POST /v1/auth:

  • Signed RS256 with the server's private key; the worker verifies with the matching public key.
  • The token embeds the caller's forwarded IP/host and expires after 1 hour.
  • assets/session-manager.js fetches, caches (in sessionStorage), and refreshes it. Use window.theme.sessionManager.getToken() for authenticated calls.

The OAuth and webhook routes are not JWT-protected (they're secured by Shopify HMAC instead).

Endpoints

Used by the theme

EndpointMethodPurpose
/v1/authPOSTIssue a 1-hour JWT.
/v1/vehicle/{state}/{rego}GETLook up vehicle(s) by registration + state (AutoInfo).
/v1/vehicle/{state}/{vehicleId}/partsGETBattery fitment for a vehicle, enriched with Shopify product data.
/v1/postcode/{postcode}GETResolve a postcode to suburb/state (Australia Post).
/v1/places?q=GETAddress autocomplete (Google Places).
/v1/places/{placeId}GETStructured address for a selected place.
/v1/scheduleGETInstallation slot availability (Google Calendar). Requires a postcode query param - see below.
/v1/schedule/reservePOSTReserve a slot and provision an Order ID.
/v1/schedule/slot/{slotId}/statusGETWhether a slot is open, occupied or invalid. With an orderId query param it also says whether that order holds the slot (heldByOrder) and returns the booked date, time and vehicle. Used by the cart booking check.
/v1/contactPOSTContact-form submission (sends email).

Platform / integration (not called by the storefront JS)

EndpointMethodPurpose
/v1/oauth/start, /v1/oauth/callbackGETShopify app OAuth onboarding (stores the shop's Admin API token).
/v1/webhooks/order_paidPOSTShopify webhook that confirms a booking on payment.
/v1/heartbeatGETHealth check.

The /v1/.../parts response is enriched by querying the Shopify Admin API for each fitment SKU. This requires the shop to be onboarded via OAuth (its token stored in KV). If the shop isn't connected, parts lookup returns a 403 and no batteries appear - see Managing battery products and Common issues.

Scheduling requires a postcode

GET /v1/schedule reads availability from a specific installer's Google Calendar, and the only thing that tells the API which calendar to read is the customer's postcode. The request the theme sends is:

GET /v1/schedule?start=2026-06-30&format=YYYY-mm-dd&postcode=2000
Query paramRequiredMeaning
startyesThe date to read availability from (YYYY-MM-DD).
formatyesDate format hint for the response.
postcodeyesThe customer's resolved postcode. Maps to the area's installer calendar URL (from its postcode_capabilities metaobject - see Delivery zones).

A /v1/schedule call without a postcode is an incomplete request and returns a 400. Always include it. The postcode is what resolves the installer calendar; without it the API has no calendar to query.

Because of this, the theme is deliberate about when it calls /v1/schedule:

  • It does not call it on page load. The call is deferred until the customer reaches the "Pick a time slot" step, by which point a postcode has been resolved and persisted.
  • The postcode is resolved either from the header postcode entry, or - during the "Confirm your details" step - from the Google Places address the customer picks: the theme runs that address's postcode back through /v1/postcode/{postcode} and persists the full record (suburb, state, eligibility booleans, and calendar URL).
  • The theme reads the persisted postcode via window.theme.postcodeEntry.getPostcode() when building the request (assets/installation.js → loadAvailabilities).

The response is grouped by calendarId; the theme additionally shows only the slots whose calendarId matches the postcode's stored calendar URL, so a customer never sees another area's slots. See Where data lives for the persisted postcode fields.

The booking lifecycle (reserve → pay → confirm)

The Order ID ties a calendar slot, a database record, and a Shopify order together across three steps:

  1. Reserve - POST /v1/schedule/reserve takes the slot, customer, vehicle and battery details. It flips the Google Calendar event from "Open Slot" to "[UNCONFIRMED] Installation for …", inserts an unconfirmedOrders row (a UUID = the Order ID, confirmedAt = null), emails a notification, and returns the Order ID. The theme attaches it to both cart line items.
  2. Pay - the customer checks out in Shopify. The order carries the _Order ID line-item property.
  3. Confirm - Shopify fires order_paid → POST /v1/webhooks/order_paid. The worker finds the booking by _Order ID, sets confirmedAt, converts the calendar event to a permanent "Installation for …" event, and emails the customer and office.

Unpaid reservations expire. An hourly cron job releases any unconfirmedOrders older than ~1 hour that were never confirmed. It reverts the calendar event back to "Open Slot" and stamps the row's expiredAt (rows are never deleted). So a slot a customer reserved but didn't pay for is returned to availability automatically. This also means if the order_paid webhook is not configured or fails, a genuinely paid booking can be auto-released - see Common issues.

Paying after the reservation has expired

A Shopify cart outlives the reservation by days, and the Shop app sends customers back to old carts through links that go straight to checkout, so a customer can pay for a booking whose slot has already been released. When order_paid finds the booking expired (or never reserved), it checks the slot:

  • Still open: the slot is reserved again and the booking is confirmed as normal.
  • Taken, gone, or the calendar cannot be checked: nothing is booked, and an "ACTION REQUIRED" email goes to the office address with the customer, the booked time and the slot's current status, so someone can reschedule. The same email is sent when the _Order ID has no booking record at all.

On the storefront, the theme catches most of these before payment. See Returning to an old cart.

External providers

ProviderUsed byConfigured via (secret/var names)
AutoInfo (vehicle + parts)/v1/vehicle/*AUTOINFO_USER_ID, AUTOINFO_AUTH_CODE
Australia Post (postcodes)/v1/postcode/*AUSPOST_API_KEY
Google Calendar (slots)/v1/schedule*, webhookGOOGLE_APPLICATION_CREDENTIALS, GOOGLE_IMPERSONATE_EMAIL
Gmail (notifications)reserve, contact, webhookGOOGLE_APPLICATION_CREDENTIALS, GOOGLE_IMPERSONATE_CONTACT_EMAIL
Google Places (addresses)/v1/places*GOOGLE_MAPS_API_KEY
Shopify Admin API (product enrichment, OAuth)/v1/.../parts, OAuthSHOPIFY_APP_CLIENT_ID, SHOPIFY_APP_SHARED_SECRET, SHOPIFY_DEFAULT_SHOP
JWT keys/v1/auth, middlewareJWT_PRIVATE_KEY, JWT_PUBLIC_KEY

These names come from the backend's .dev.vars.example / .env.example. Values are stored as Cloudflare Worker secrets / wrangler vars - never in this handbook or the theme.

Database (D1)

Two tables (Drizzle schema, src/db/schema.ts in the backend repo):

  • installers - each installer's email and Google Calendar ID. Availability is read from these calendars. See Appointments & scheduling.
  • unconfirmedOrders - a booking record per reservation: the Order ID (UUID), vehicle/battery, customer, slot/calendar IDs, booking date/time, createdAt, reservedAt, confirmedAt and expiredAt.
  • order_events - an append-only audit trail. Every provision, reservation, expiry, payment, recovery and staff alert is recorded against the Order ID with the full payload as JSON. Worker logs only last 7 days, so this is where to look for anything older.

Environments & deployment

Two Cloudflare accounts, two wrangler configs (see Who owns what):

ProductionStaging
OwnerCar Battery ProConversion Kings
Wrangler configwrangler.jsoncwrangler-ck.jsonc
Worker nameeb-cbp-apicbp-rego-lookup-api
Deploy commandnpm run deploy:prodnpm run deploy

The theme points at production (the api_base_url setting) in the live store, and at staging when debug mode is on (api_dev_base_url).

For a future handover: the API is a separate repository and infrastructure from this theme. Production Cloudflare resources are owned by Car Battery Pro; the staging account belongs to Conversion Kings. A new maintainer needs the eb-cbp-rego-lookup repo, the Cloudflare accounts, all the provider credentials above, and the Shopify app (client ID/secret + order_paid webhook). Confirm exact access during handover.

On this page