Build a booking flow end to end
From an empty basket to an active, billing licence — in eight calls.
Build a booking flow end to end
From an empty basket to an active, billing licence — in eight calls.
This walks through taking a customer from "browsing availability" to "active, billing agreement" using only the public API — chaining availability → contact → reservation → order → agreement (licence) → payment → subscription, with the events emitted at each step. It's the canonical proof that StoreBay is genuinely buildable-on: nothing here is internal-only.
The worked example throughout: Grace Adeyemi books a Small 25 unit at Pennine Self
Storage · Attercliffe. Licence fee 2900 + Plus waiver 800 = total_minor: 3700
("currency": "GBP"). She pays by Bacs Direct Debit — the default recurring rail. All ids
below are illustrative UUIDv7 strings.
The call sequence
- Authenticate — a credential scoped for the resources you'll touch.
- Check availability —
GET /v1/availabilityfor the site and unit type. - Create the customer —
POST /v1/contacts. - Hold the unit —
POST /v1/reservations. - Build the order —
POST /v1/orders, then add the unit line, a padlock add-on, and the waiver. - Sign the licence —
POST /v1/agreements+ an e-signcontract; the customer completes signing. - Set up Direct Debit —
POST /v1/mandates; the customer completes GoCardless's hosted setup. - Take the first payment —
POST /v1/payments. - Go live —
POST /v1/subscriptions— recurring billing now runs itself.
Each hop fires an event so anything listening (your own backend, or StoreBay's own comms/billing
engines) reacts without polling:
contact.created → reservation.created → order.created → agreement.signed → mandate.active
→ payment.succeeded → subscription.created.
Note
Send an Idempotency-Key on every mutation so a retry can't double-book or double-charge.
0 · Authenticate
Use a credential scoped for what this flow touches: units:read, contacts:write,
reservations:write, agreements:write, compliance:write, billing:write. See
Authentication for how to get one — build against sb_test_… in the
sandbox first.
curl https://api.storebay.co.uk/v1/sites \
-H "Authorization: Bearer sb_test_…"1 · Check availability
GET /v1/availability — no write, no event; it's a derived read. Scope: units:read.
curl "https://api.storebay.co.uk/v1/availability?site_id=018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e01&unit_type_id=018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e02" \
-H "Authorization: Bearer sb_test_…"{
"data": [
{
"id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e03",
"object": "availability",
"site_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e01",
"unit_type_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e02",
"available_units": 4,
"price_minor": 2900,
"currency": "GBP"
}
],
"meta": { "limit": 25, "cursor": { "next": null, "has_more": false } }
}Attercliffe has a Small 25 free at 2900 (£29.00) per month.
2 · Create the customer
POST /v1/contacts. Scope: contacts:write. Fires contact.created.
curl -X POST https://api.storebay.co.uk/v1/contacts \
-H "Authorization: Bearer sb_test_…" \
-H "Idempotency-Key: 8f1a2b3c-4d5e-4f60-8a1b-2c3d4e5f6a7b" \
-H "Content-Type: application/json" \
-d '{
"kind": "customer",
"first_name": "Grace",
"last_name": "Adeyemi",
"email": "grace.adeyemi@example.com",
"phone": "+44 7700 900123",
"address_line1": "14 Effingham Court",
"city": "Sheffield",
"postcode": "S9 4LB",
"country_code": "GB",
"marketing_consent": true
}'{
"data": {
"id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e10",
"object": "contact",
"kind": "customer",
"first_name": "Grace",
"last_name": "Adeyemi",
"email": "grace.adeyemi@example.com",
"created_at": "2026-07-06T09:00:00Z"
}
}3 · Hold the unit
POST /v1/reservations. Scope: reservations:write. Fires reservation.created.
curl -X POST https://api.storebay.co.uk/v1/reservations \
-H "Authorization: Bearer sb_test_…" \
-H "Idempotency-Key: 1a2b3c4d-5e6f-4071-8a1b-2c3d4e5f6a7c" \
-H "Content-Type: application/json" \
-d '{
"site_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e01",
"unit_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e04",
"contact_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e10",
"source": "api",
"quoted_price_minor": 2900,
"currency": "GBP",
"move_in_date": "2026-07-08",
"held_until": "2026-07-06T18:00:00Z"
}'{
"data": {
"id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e11",
"object": "reservation",
"site_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e01",
"unit_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e04",
"contact_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e10",
"status": "held",
"quoted_price_minor": 2900,
"currency": "GBP",
"held_until": "2026-07-06T18:00:00Z"
}
}Unit A-118 (Small 25) at Attercliffe is now held against Grace — nobody else can book it while the hold lasts.
4 · Build the order
Create the order, then declare its basket — the unit, a padlock add-on, and the Standard coverage
waiver. Scope: reservations:write. The create fires order.created; each PATCH re-prices the
basket in place.
curl -X POST https://api.storebay.co.uk/v1/orders \
-H "Authorization: Bearer sb_test_…" \
-H "Idempotency-Key: 2b3c4d5e-6f70-4182-8a1b-2c3d4e5f6a7d" \
-H "Content-Type: application/json" \
-d '{
"site_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e01",
"contact_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e10",
"reservation_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e11",
"channel": "api",
"currency": "GBP"
}'curl -X PATCH https://api.storebay.co.uk/v1/orders/018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e12 \
-H "Authorization: Bearer sb_test_…" \
-H "Content-Type: application/json" \
-d '{
"coverage_product_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e05",
"include_deposit": false
}'{
"data": {
"id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e12",
"object": "order",
"status": "draft",
"subtotal_minor": 3084,
"tax_minor": 616,
"total_minor": 3700,
"currency": "GBP",
"lines": [
{ "object": "order_line", "line_type": "unit", "unit_price_minor": 2900, "recurring": true },
{ "object": "order_line", "line_type": "coverage", "unit_price_minor": 800, "recurring": true }
]
}
}total_minor: 3700 (£37.00) — the Small 25's 2900 licence fee plus the Plus waiver's 800,
both VAT-inclusive.
5 · Sign the licence
Place the order, then create the agreement (the UK storage licence — never a lease) and an
e-sign contract. Scopes: reservations:write, agreements:write, compliance:write. Fires
agreement.created, then — once Grace actually signs — contract.signed and agreement.signed.
curl -X POST https://api.storebay.co.uk/v1/orders/018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e12/place \
-H "Authorization: Bearer sb_test_…" \
-H "Idempotency-Key: 3c4d5e6f-7081-4293-8a1b-2c3d4e5f6a7e"curl -X POST https://api.storebay.co.uk/v1/agreements \
-H "Authorization: Bearer sb_test_…" \
-H "Idempotency-Key: 4d5e6f70-8192-40a4-8a1b-2c3d4e5f6a7f" \
-H "Content-Type: application/json" \
-d '{
"site_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e01",
"unit_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e04",
"contact_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e10",
"order_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e12",
"licence_fee_minor": 2900,
"currency": "GBP",
"billing_interval": "4week",
"move_in_date": "2026-07-08"
}'{
"data": {
"id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e13",
"object": "agreement",
"status": "awaiting_signature",
"licence_fee_minor": 2900,
"currency": "GBP",
"billing_interval": "4week",
"move_in_date": "2026-07-08"
}
}Create the e-sign envelope against that agreement:
curl -X POST https://api.storebay.co.uk/v1/contracts \
-H "Authorization: Bearer sb_test_…" \
-H "Idempotency-Key: 5e6f7081-92a3-41b5-8a1b-2c3d4e5f6a80" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e10",
"agreement_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e13",
"title": "StoreBay Storage Licence — Grace Adeyemi / A-118"
}'Signing itself happens where Grace is — the portal or an embedded signing UI, not a direct API
call an integrator makes on her behalf. Once she signs, the contract moves sent → signed → countersigned, and the agreement advances to pending_move_in (or active, if move-in is
already today) — both firing on the event stream you registered in the next step.
6 · Set up Direct Debit
POST /v1/mandates. Scope: billing:write. Fires mandate.created.
curl -X POST https://api.storebay.co.uk/v1/mandates \
-H "Authorization: Bearer sb_test_…" \
-H "Idempotency-Key: 6f708192-a3b4-42c6-8a1b-2c3d4e5f6a81" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e10",
"reference": "PENN-A-0207",
"scheme": "bacs"
}'{
"data": {
"id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e14",
"object": "mandate",
"contact_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e10",
"provider": "gocardless",
"reference": "PENN-A-0207",
"status": "awaiting_authorisation",
"scheme": "bacs"
}
}Grace completes GoCardless's hosted Direct Debit setup; StoreBay confirms the mandate over the
next 2–4 working days and fires mandate.active the moment it's usable.
7 · Take the first payment
POST /v1/payments, against the now-active mandate. Scope: billing:write.
curl -X POST https://api.storebay.co.uk/v1/payments \
-H "Authorization: Bearer sb_test_…" \
-H "Idempotency-Key: 708192a3-b4c5-43d7-8a1b-2c3d4e5f6a82" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e10",
"mandate_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e14",
"rail": "bacs_dd",
"amount_minor": 3700,
"currency": "GBP"
}'{
"data": {
"id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e15",
"object": "payment",
"contact_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e10",
"mandate_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e14",
"provider": "gocardless",
"rail": "bacs_dd",
"amount_minor": 3700,
"currency": "GBP",
"status": "submitted",
"settled_final": false
}
}Warning
The first collection is submitted, not settled — it clears in ~3 working days, and can still be
reversed. payment.succeeded fires only once GoCardless confirms it's actually collected — don't
treat the 201 response above as money in the bank.
8 · You're live
POST /v1/subscriptions, against the signed agreement. Scope: agreements:write. Fires
subscription.created.
curl -X POST https://api.storebay.co.uk/v1/subscriptions \
-H "Authorization: Bearer sb_test_…" \
-H "Idempotency-Key: 8192a3b4-c5d6-44e8-8a1b-2c3d4e5f6a83" \
-H "Content-Type: application/json" \
-d '{
"agreement_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e13",
"contact_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e10",
"site_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e01",
"billing_interval": "4week",
"amount_minor": 2900,
"currency": "GBP",
"mandate_id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e14",
"default_rail": "bacs_dd",
"auto_collect": true
}'{
"data": {
"id": "018f9c2a-7b3e-7c1a-9f2d-3a5b6c7d8e16",
"object": "subscription",
"status": "active",
"billing_interval": "4week",
"amount_minor": 2900,
"currency": "GBP",
"default_rail": "bacs_dd",
"auto_collect": true
}
}Recurring billing now runs itself — StoreBay's billing engine creates the next invoice, collects via the mandate, and everything downstream (dunning, overlock, access) reacts to the same event stream you're already listening to. You're live.
Entities and events
Availability
Contacts
Reservations
Orders
Agreements
Mandates
Payments
Subscriptions
What to handle next
- Handle the events — register an endpoint and react to
agreement.signed,mandate.active,payment.succeeded, and the rest, instead of polling. - Do it in a test operator — run this entire flow against
Stripe test mode / GoCardless sandbox first; nothing here moves real money until you swap in a
sb_live_…credential.
StoreBay's own storefront checkout runs this exact sequence under the hood — this guide shows the same contract available to you directly.