The API is included in the plan. The account owner creates keys in the developer settings. For a test account, email hello@storebay.co.uk.

Developers

Changelog

Every change to the API, webhooks, and SDKs — newest first.

Every change to the API, webhooks, and SDKs — newest first. /v1 changes are additive; anything breaking ships as /v2.

How entries work

Each entry is a dated heading with one or more tagged changes — Added · Changed · Deprecated · Sunset · Fixed — always a text label, never colour alone. A deprecation reads:

Deprecated {date} · sunset {date} — see the migration guide.

See Conventions for the full versioning and deprecation policy — what's additive vs. breaking, and the Deprecation/Sunset response headers.

/v1 only ever grows additively (a new endpoint, a new optional field, a new enum value, a new event type); a change that would break an existing integration ships as /v2 instead, announced here with its own migration guide, per StoreBay's versioning and deprecation policy.

7 Oct 2026

Added

  • Invoice gains void_reason and voided_from. voided_from is the status the invoice had when it was voided (draft, open or overdue). void_reason is why a member of staff voided it, as they picked it; it is null on an invoice voided through the API, on a draft, and on a void made before 7 Oct 2026. Both are null on an invoice that is not void. The invoice.voided webhook carries them, so a subscriber can tell a voided draft (which never counted) from a voided invoice that had been issued.
  • The error code invoice_busy (409): another writer held something a void needed, past five seconds. Nothing was written; try again in a moment.

Changed

  • POST /v1/invoices/{id}/void now applies the same rule, and does the same things, as voiding an invoice in the dashboard. This is a deliberate change to /v1 in place, approved by the founder on 6 Oct 2026: a void that left the customer's arrears case, the reminder emails and the books as they were was wrong for every caller. What it changes for an API caller:
    • An overdue invoice can now be voided. StoreBay switches that on operator by operator; until it is on for yours, an overdue invoice is refused 409 as before.
    • These are refused 409, in words, before anything is written: an invoice with money against it (a part-payment included) or a credit note issued or applied against it, whose debt was written off or is part of a sale of goods, or with a collection still with the provider. details[0].issue names which (invoice_part_paid, invoice_money_against_it, invoice_credit_note_against_it, invoice_written_off, invoice_debt_in_sale, invoice_collection_in_flight, invoice_shop_sale, invoice_not_voidable). A void of an open invoice with a part-payment or a credit note against it used to succeed, and counted that money twice; it is the one edge that can now fail where it used to work.
    • A void now also closes the customer's arrears case, and requests an unlock of their unit, when nothing overdue is left (otherwise it refreshes the case's amount); withdraws the reminder emails still waiting to go for the invoice; closes the invoice's failed collection attempts and cancels their card payments; cancels a refundable deposit that was never paid; and voids the invoice in Xero or QuickBooks, where it used to be pushed as if it still stood. It never emails the customer and never waives a late fee: those are choices a member of staff makes in the dashboard.
    • Voiding an invoice that is already void is still a 200 that changes nothing.
    • amount_due_minor on a void invoice is what was due when it was voided; nothing is owed on it.

3 Oct 2026

Added

  • Online ID checks: a customer checks their ID online (a photo of their ID and a live selfie, checked by a specialist provider), and where an operator asks for it, access to a booking waits until the check is done. StoreBay switches this on operator by operator; until it is on for you, nothing below changes what your customers see.
  • POST /v1/identity-verifications emails the customer a link to check their ID online. It answers with the request and never the link. It answers 409 when the operator has not turned online ID checks on (id_check_link names why) and 422 when the customer has no email address; it used to answer 422 to every call. Send an Idempotency-Key: each call emails a new link.
  • GET /v1/identity-verifications and GET /v1/identity-verifications/{id} read the online ID checks customers have started. provider gains didit. A check a member of staff accepted (the same person under another form of their name) reads as verified.
  • The webhook events agreement.access_held (a move-in kept the customer's access back until their ID is checked) and agreement.access_released (the access that was held has been given), and identity_verification.verified, identity_verification.requires_input and identity_verification.canceled. Do not grant access on agreement.signed while an agreement.access_held for the booking has not been followed by agreement.access_released.
  • PATCH /v1/agreements/{id} answers 409 (conflict) to a different contact_id while the booking's access is waiting for an online ID check: the check is the customer's, so the booking keeps its customer until they are let in or the booking ends.

Changed

  • kyc_status on Contact (deprecated) is derived rather than stored, for every operator and on the contact.* webhooks: not_required when the operator does not check ID, verified when every document it asks for is checked (at the desk, on a video call or online), failed when it is not and the customer's latest online check is waiting for a member of staff, and pending otherwise. rejected is no longer served. Read the customer's check from GET /v1/identity-verifications?contact_id= instead.

2 Oct 2026

Added

  • Staff reservations: a unit held for a customer until their move-in date. POST /v1/agreements takes reservation_id to book the unit a reservation holds, taking the reservation up (converted); booking a unit reserved for the same customer takes it up without naming it. When the booking is of that one unit, an omitted licence_fee_minor and move_in_date are the reservation's price and move-in date (today, once that date has passed).
  • reservation_id on Agreement (the reservation it took up) and on Deal (the reservation holding a unit for the customer); coupon_id on Reservation (the promotion offered).
  • GET /v1/units?bookable=true&reservation_id= counts the unit that reservation holds as bookable.
  • POST /v1/subscriptions with no promotion_code, for an agreement that took up a reservation offering a promotion, puts that promotion on the plan, once.
  • An open deal in its reserved stage whose reservation is cancelled or lapses goes back to New with a call due today, unless the customer still has another reservation or booking at that site, or the operator switched this off.
  • The unit_reserved error code (409): the unit is held for another customer until a date, or is being booked online. The message names the hold.
  • credit_note_id on Payment, and on the data of a payment webhook: the credit note money was paid back against. It is set on the refund a Shop return pays out at the desk (the return's credit_note.created names the same id), so a client that hears payment.refunded for it can tie the two together. It is null on every other payment.
  • provider_charge_date, bank_submitted_at and superseded_by_operator_at on Payment, and on the data of a payment webhook. For a Bacs Direct Debit they are the day the provider debits the customer (a YYYY-MM-DD date), when it submitted the collection to their bank, and when the operator stopped waiting on it. provider_charge_date and bank_submitted_at are null on a card or offline payment, and until they apply. superseded_by_operator_at is null until the operator stops waiting, on any payment, so a card payment they gave up on carries it too.
  • Promotions on a plan. POST /v1/subscriptions/{id}/promotion puts one on and DELETE /v1/subscriptions/{id}/promotion takes it off. Subscription.promotion says what it takes off, the periods it discounts and how many it has used, and POST /v1/subscriptions takes a promotion_code. Coupons gain auto_apply (a code typed at checkout beats it; then the biggest first-year saving wins, then the oldest), online, site_ids and unit_type_ids.
  • Marketing consent in three states. marketing_status on Contact is yes, no or not_asked; the create and update bodies take marketing_status and marketing_consent_wording (the words the person was shown). marketing_consent stays and is true exactly when the status is yes. A message sent as an offer to a no is never handed to a provider: blocked_reason on Message is marketing_refused.
  • Enquiries. Deal gains channel, unit_type_id, need_by_date, agreement_id, quoted_at, reserved_at, last_activity_at, next_follow_up_on and next_follow_up_window. Create and update take channel, unit_type_id, need_by_date, next_follow_up_on and next_follow_up_window, and update also takes agreement_id. POST /v1/deals takes a note (trimmed; over 2000 characters is a 422). The no_longer_due value of blocked_reason: a staff email that had nothing left to send by the time its turn came.
  • Products gain accounting_code, usable_website (and a usable_website filter on GET /v1/products), usable_shop, track_stock, price_mode, unit_label, unit_price_e4 and standard_rate_above_per_day. vat_treatment takes reduced_5 (5% VAT) on products, order lines and the items of POST /v1/subscriptions.
  • offline_method on POST /v1/payments gains card_terminal, standing_order and other; other needs a reference.
  • Insurance. Coverage products gain insurer_name, pricing_mode, rate_per_thousand_minor, min_cover_minor, max_cover_minor and rounding_step, and an invoice carries the insurance premium tax as ipt_minor (on the invoice.* events too).
  • GET /v1/reports/conversion rows gain group_by, label, quoted and reserved; the CSV export ends with the same four columns.
  • system_role on Contact: walk_in for the record the Shop keeps for a sale to someone with no customer record (named "Walk-in sales", one per operator), null for every customer. GET /v1/contacts never lists it.
  • expected_amount_minor on PATCH /v1/subscriptions/{id}, sent with amount_minor: when the subscription's amount is no longer that figure the change is a 409 conflict and nothing is written.
  • The reservation.expired webhook: a hold ran out before it was booked or cancelled, found by the 15-minute sweep or by staff acting on it first. data is the reservation, with status expired. No email is sent. payment.refunded is also sent for money paid back at the desk for a Shop return; its data is the refund itself, a new payment with direction refund.
  • The address_reserved value of blocked_reason on Message: the address is at a reserved domain such as example.com, where no mailbox can exist.
  • The payment_not_received value of blocked_reason on Message: a receipt was held because the payment it would thank the customer for was not received, or was returned before it reached the operator.
  • amenities and description on Site, as the operator's websites show them: what the site offers, each amenity once from a fixed list and in its order (an empty list when none is recorded), and the site in its own words (null when there is none).
  • advertised on Coupon, and on its create and update bodies: the coupon is shown on the operator's websites while it can be redeemed, and their booking links carry it to checkout. Only a once coupon that takes a percentage or a fixed amount off, is online and applies at every site and unit type can be advertised.
  • return_url on POST /v1/orders/{id}/pay, for bacs_dd: where GoCardless sends the customer once they authorise the Direct Debit. It must be the checkout's confirmation page on one of the operator's own published website addresses, or the call is a 422 on return_url. Without one the customer returns to the StoreBay customer portal.

Changed

  • POST /v1/reservations: source is storefront, backoffice or api; any other value is a 422.
  • POST /v1/reservations/{id}/cancel refuses a hold whose held_until has passed: 409 "This reservation has already lapsed."
  • POST /v1/orders refuses a reservation_id made for a deal (409): book it with POST /v1/agreements instead.
  • DELETE /v1/deals/{id} refuses a deal a unit is reserved for (409): cancel the reservation first. Marking such a deal lost cancels its reservation.
  • POST /v1/subscriptions/{id}/cancel refuses (422 validation_error on effective_date) a value that is not a calendar date in YYYY-MM-DD form (year 0000 included), and a date before the day the plan began: its agreement's move-in date, else the day the plan started. It used to answer 500, or read a word such as epoch as a date. A plan with neither day recorded has no lower bound.
  • GET /v1/events: type is the public name, the one a webhook delivers. invoice.issued reads invoice.finalized, agreement.activated agreement.signed, credit_note.issued credit_note.created, identity_verification.cancelled identity_verification.canceled and payment.refund_recorded payment.refunded, as the webhook documentation has always said. The feed used to show the internal name.
  • actor_type on the events of a member of staff or of StoreBay support working through the API now says so. payment.initiated for a card charge a staff token starts is staff (it was api); credit.added for money recorded at the desk that leaves credit is staff or platform_admin (it was provider); enquiry and plan-promotion events written in a StoreBay support session are platform_admin (they were staff or api).
  • GET /v1/reports/delinquency: overdue_minor and overdue_accounts count only the invoices in the operator's currency, as the report's collection rate always did. They used to add every currency together.
  • GET /v1/reports/occupancy: economic_actual_minor, and so economic_occupancy_pct, is net of the promotion on each plan, as the revenue report's MRR is. It used to count the full licence fee.
  • GET /v1/reports/revenue: mrr_minor, arr_minor and revpau_minor are net of each plan's active promotion.
  • POST /v1/payments: a card or Direct Debit amount under what the provider collects in one payment is a 422 on amount_minor, issue below_minimum: 30p for a card and £1.00 for Direct Debit in GBP. An offline recording has no minimum. POST /v1/orders/{id}/pay refuses a card payment for an order that comes to nothing, or to less than the least the gateway charges, with the same detail, before any payment is written. PATCH /v1/orders/{id} answers 422 on coupon_code for a promotion that would leave a website order's first card payment at nothing or under that least.
  • A Bacs Direct Debit is succeeded when GoCardless confirms the collection, not when it is paid out. Its invoice is paid then, payment.succeeded and invoice.paid are sent then, and charged_at is the confirmation; paid_out follows days later and sends no event. A collection returned before the payout is failed (payment.failed) and its invoice is reopened; one returned after it is late_failure_settled or chargeback_settled, as before.
  • POST /v1/payments with rail bacs_dd against an invoice asks the bank to debit on the day the customer's advance notice named, never earlier, and once that day can no longer be met, on the first day the bank can, as the operator's "collect now" does. Before, the request carried no date and was debited on the first day the bank could, which could be days ahead of the notice.
  • An invoice collected by Direct Debit is due, and its notice and collection are dated, on a working day: a date that would fall on a weekend or bank holiday is the next working day, so the day the customer is told is the day the bank debits. Invoice.due_date moves with it.
  • An invoice's status can go from overdue back to open: while a collection that covers what it owes is in flight (card or Direct Debit), and while a Direct Debit advance notice for exactly what it owes names a charge date that has not yet come. It goes back to overdue if nothing has been collected once that day has passed. A client that reads overdue as one-way should read the current status.
  • POST /v1/subscriptions/{id}/cancel bills the charges waiting for the plan's next invoice, with the late fees no other plan of the agreement will bill, at once on an invoice of their own. The cover the plan bills ends on the last day an invoice still stands for. A declined attempt no longer stops an invoice for a period that has not started being voided.
  • POST /v1/invoices/{id}/void puts the charges the invoice carried back on the plan's next invoice, unless the plan has ended.
  • PATCH /v1/subscriptions/{id}: a new billing_interval or interval_count re-prices every repeating charge and cover from the next invoice (the licence fee moves only by an amount_minor sent with it); a charge that cannot be priced for the new period refuses the whole change with 409 conflict. On a plan with no licence fee, a change to amount_minor is a 409 conflict, and an amount_minor equal to the current one is no change. On a pending plan, a next_billing_date on or before the end of a period already invoiced is a 409 conflict, and so is one while cover or a repeating charge starts on the current next_billing_date or the agreement's move_in_date: move the move_in_date instead. Clearing collection_waived or binding a card ends a standing order.
  • PATCH /v1/agreements/{id}: a move_in_date change moves the cover and repeating charges the plan was given from it, billed or not, and an explicit null is a 409 while any starts on it. A different unit_id moves a quote's or draft's booking and its hold, and transfers the customer on an agreement that is pending move-in, active or notice-given; any other agreement keeps its unit (409). GET /v1/agreements now applies status, contact_id and site_id, and answers 400 to a value that is not valid.
  • POST /v1/deals with no pipeline_id puts the deal on the operator's pipeline, and with no stage_id at that pipeline's first stage. quoted_at and reserved_at are stamped as they happen. DELETE /v1/deals/{id} refuses a deal a quote was sent from (409): mark it lost instead. Marking a deal lost while its reservation is busy is a 409 ("This reservation is busy right now. Try again in a moment.").
  • PATCH /v1/deals/{id}: status won or lost closes the deal, moves it to a matching stage and clears its next call; a lost deal can be reopened with open; a won deal's status cannot change; a won or lost deal cannot be moved to an open stage; and the pipeline's won or lost stage sent with status open is a 422. A deal lost without a reason keeps the one already stored on it, or else gets the operator's "Not recorded" reason. A member of staff (a backoffice token) wins an enquiry only by naming agreement_id (422 without one); an API key or OAuth client is unchanged. Changing contact_id on a deal linked to an agreement is a 422 on agreement_id when the agreement belongs to another customer. A body of only null values is accepted.
  • PATCH /v1/coupons/{id} refuses (409) a change to what a coupon takes off (discount_kind, discount_value, currency, duration, duration_months) while a live quote carries it. A once coupon discounts the licence fee only.
  • POST /v1/credit-notes refuses (409 conflict) an invoice that bills insurance (ipt_minor above 0): void it instead, if nothing has been paid. It refuses the invoice of a Shop sale too: return its items from the sale.
  • Coverage products: PATCH /v1/coverage-products/{id} is a 409 cover_in_force when customers hold the product and the change would alter what their cover is. A waiver whose name says insurance is a 422 on name, as are a rate_per_thousand_minor above 100000 and a regulatory_basis_ref over 200 characters or with a non-printable one. billing_cycle is monthly only (weekly, annual and one_off are a 422). PATCH /v1/orders/{id} takes a coverage_product_id only for an active fixed-level monthly waiver whose name does not say insurance (422), and an explicit null removes the cover.
  • Contacts: marketing_consent false on create records nothing, and on update withdraws an earlier yes. A no cannot be set back to not_asked (422).
  • POST /v1/subscriptions with no items makes one licence-fee line at the unit type's VAT treatment. A coverage item is refused (422 on items[i].item_type); coverage leaves that enum.
  • POST /v1/messages answers 422 on contact_id for an unknown contact or the walk-in record. A backoffice staff token reads [link hidden] in place of a link that lets someone act as the customer (sign-in, signing, card set-up, Book); an API key or OAuth client reads the body as stored.
  • POST /v1/notes: subject_type is agreement, collections_case, contact, deal or unit (422 otherwise; existing notes keep theirs), and a contact subject that is the walk-in record is a 422 on subject_id.
  • The walk-in record is refused as the customer on the /v1 writes for agreements, reservations, orders, deals, notes and messages (422), and it cannot be changed or archived (409).
  • Payments recorded with a staff token: payment.succeeded and invoice.paid name staff as the actor, not api.
  • contact.unarchived is also sent when an archived customer returns by enquiring, booking or being added again, and contact.updated when a customer saves their own details in the portal.
  • An order left awaiting_payment for over a day with nothing in flight is ended as abandoned.

Fixed

  • POST /v1/agreements no longer books a unit that is held for someone else, archived or not in service: a 409 says which. One booking takes up at most one reservation, so two reserved units are a 422.
  • POST /v1/deals and PATCH /v1/deals/{id} check the customer, site, unit type and agreement they name against the caller's sites (403 site_forbidden).
  • The event catalogue said reservation.cancelled also covered a hold that ran out. It means a reservation cancelled through the API, by staff, or because its deal was marked lost; a hold that runs out sends reservation.expired.

6 Jul 2026

Added

  • The complete /v1 REST API — catalogue, commerce, billing, CRM, compliance, access, integrations, comms, and platform resources — with a generated, interactive API reference and a try-it panel on every endpoint.
  • Signed, versioned webhooks with the full v1 event catalogue (including deal.*, task.*, note.*, import.*, and webhook_endpoint.disabled), ~8-attempt exponential-backoff retries, manual redelivery, and auto-disable on sustained failure.
  • OAuth2 client-credentials and authorization-code + PKCE (for marketplace apps), plus the refresh_token grant.
  • A sandbox: a separate test account with identical isolation to a live operator.
  • The booking-flow guide — the full availability → contact → reservation → order → agreement → payment → subscription chain, worked end to end.

Note

Subscribing to this changelog (RSS/Atom) is planned but not built yet — check back here for now.

On this page