StoreBay Developers
Guides

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

  1. Authenticate — a credential scoped for the resources you'll touch.
  2. Check availabilityGET /v1/availability for the site and unit type.
  3. Create the customerPOST /v1/contacts.
  4. Hold the unitPOST /v1/reservations.
  5. Build the orderPOST /v1/orders, then add the unit line, a padlock add-on, and the waiver.
  6. Sign the licencePOST /v1/agreements + an e-sign contract; the customer completes signing.
  7. Set up Direct DebitPOST /v1/mandates; the customer completes GoCardless's hosted setup.
  8. Take the first paymentPOST /v1/payments.
  9. Go livePOST /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.createdreservation.createdorder.createdagreement.signedmandate.activepayment.succeededsubscription.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

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.

On this page