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
Invoicegainsvoid_reasonandvoided_from.voided_fromis the status the invoice had when it was voided (draft,openoroverdue).void_reasonis why a member of staff voided it, as they picked it; it isnullon an invoice voided through the API, on a draft, and on a void made before 7 Oct 2026. Both arenullon an invoice that is not void. Theinvoice.voidedwebhook carries them, so a subscriber can tell a voided draft (which never counted) from a voided invoice that had been issued.- The error
codeinvoice_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}/voidnow applies the same rule, and does the same things, as voiding an invoice in the dashboard. This is a deliberate change to/v1in 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
409as 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].issuenames 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
200that changes nothing. amount_due_minoron avoidinvoice is what was due when it was voided; nothing is owed on it.
- 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
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-verificationsemails the customer a link to check their ID online. It answers with the request and never the link. It answers409when the operator has not turned online ID checks on (id_check_linknames why) and422when the customer has no email address; it used to answer422to every call. Send anIdempotency-Key: each call emails a new link.GET /v1/identity-verificationsandGET /v1/identity-verifications/{id}read the online ID checks customers have started.providergainsdidit. A check a member of staff accepted (the same person under another form of their name) reads asverified.- The webhook events
agreement.access_held(a move-in kept the customer's access back until their ID is checked) andagreement.access_released(the access that was held has been given), andidentity_verification.verified,identity_verification.requires_inputandidentity_verification.canceled. Do not grant access onagreement.signedwhile anagreement.access_heldfor the booking has not been followed byagreement.access_released. PATCH /v1/agreements/{id}answers409(conflict) to a differentcontact_idwhile 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_statuson Contact (deprecated) is derived rather than stored, for every operator and on thecontact.*webhooks:not_requiredwhen the operator does not check ID,verifiedwhen every document it asks for is checked (at the desk, on a video call or online),failedwhen it is not and the customer's latest online check is waiting for a member of staff, andpendingotherwise.rejectedis no longer served. Read the customer's check fromGET /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/agreementstakesreservation_idto 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 omittedlicence_fee_minorandmove_in_dateare the reservation's price and move-in date (today, once that date has passed). reservation_idon Agreement (the reservation it took up) and on Deal (the reservation holding a unit for the customer);coupon_idon Reservation (the promotion offered).GET /v1/units?bookable=true&reservation_id=counts the unit that reservation holds as bookable.POST /v1/subscriptionswith nopromotion_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_reservederror code (409): the unit is held for another customer until a date, or is being booked online. The message names the hold. credit_note_idon Payment, and on thedataof 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'scredit_note.creatednames the same id), so a client that hearspayment.refundedfor it can tie the two together. It isnullon every other payment.provider_charge_date,bank_submitted_atandsuperseded_by_operator_aton Payment, and on thedataof a payment webhook. For a Bacs Direct Debit they are the day the provider debits the customer (aYYYY-MM-DDdate), when it submitted the collection to their bank, and when the operator stopped waiting on it.provider_charge_dateandbank_submitted_atarenullon a card or offline payment, and until they apply.superseded_by_operator_atisnulluntil 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}/promotionputs one on andDELETE /v1/subscriptions/{id}/promotiontakes it off.Subscription.promotionsays what it takes off, the periods it discounts and how many it has used, andPOST /v1/subscriptionstakes apromotion_code. Coupons gainauto_apply(a code typed at checkout beats it; then the biggest first-year saving wins, then the oldest),online,site_idsandunit_type_ids. - Marketing consent in three states.
marketing_statuson Contact isyes,noornot_asked; the create and update bodies takemarketing_statusandmarketing_consent_wording(the words the person was shown).marketing_consentstays and istrueexactly when the status isyes. A message sent as an offer to anois never handed to a provider:blocked_reasonon Message ismarketing_refused. - Enquiries. Deal gains
channel,unit_type_id,need_by_date,agreement_id,quoted_at,reserved_at,last_activity_at,next_follow_up_onandnext_follow_up_window. Create and update takechannel,unit_type_id,need_by_date,next_follow_up_onandnext_follow_up_window, and update also takesagreement_id.POST /v1/dealstakes anote(trimmed; over 2000 characters is a 422). Theno_longer_duevalue ofblocked_reason: a staff email that had nothing left to send by the time its turn came. - Products gain
accounting_code,usable_website(and ausable_websitefilter onGET /v1/products),usable_shop,track_stock,price_mode,unit_label,unit_price_e4andstandard_rate_above_per_day.vat_treatmenttakesreduced_5(5% VAT) on products, order lines and the items ofPOST /v1/subscriptions. offline_methodonPOST /v1/paymentsgainscard_terminal,standing_orderandother;otherneeds areference.- Insurance. Coverage products gain
insurer_name,pricing_mode,rate_per_thousand_minor,min_cover_minor,max_cover_minorandrounding_step, and an invoice carries the insurance premium tax asipt_minor(on theinvoice.*events too). GET /v1/reports/conversionrows gaingroup_by,label,quotedandreserved; the CSV export ends with the same four columns.system_roleon Contact:walk_infor the record the Shop keeps for a sale to someone with no customer record (named "Walk-in sales", one per operator),nullfor every customer.GET /v1/contactsnever lists it.expected_amount_minoronPATCH /v1/subscriptions/{id}, sent withamount_minor: when the subscription's amount is no longer that figure the change is a 409conflictand nothing is written.- The
reservation.expiredwebhook: a hold ran out before it was booked or cancelled, found by the 15-minute sweep or by staff acting on it first.datais the reservation, withstatusexpired. No email is sent.payment.refundedis also sent for money paid back at the desk for a Shop return; itsdatais the refund itself, a new payment withdirectionrefund. - The
address_reservedvalue ofblocked_reasonon Message: the address is at a reserved domain such asexample.com, where no mailbox can exist. - The
payment_not_receivedvalue ofblocked_reasonon 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. amenitiesanddescriptionon 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 (nullwhen there is none).advertisedon 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 aoncecoupon that takes a percentage or a fixed amount off, isonlineand applies at every site and unit type can be advertised.return_urlonPOST /v1/orders/{id}/pay, forbacs_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 onreturn_url. Without one the customer returns to the StoreBay customer portal.
Changed
POST /v1/reservations:sourceisstorefront,backofficeorapi; any other value is a 422.POST /v1/reservations/{id}/cancelrefuses a hold whoseheld_untilhas passed: 409 "This reservation has already lapsed."POST /v1/ordersrefuses areservation_idmade for a deal (409): book it withPOST /v1/agreementsinstead.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}/cancelrefuses (422validation_erroroneffective_date) a value that is not a calendar date inYYYY-MM-DDform (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 asepochas a date. A plan with neither day recorded has no lower bound.GET /v1/events:typeis the public name, the one a webhook delivers.invoice.issuedreadsinvoice.finalized,agreement.activatedagreement.signed,credit_note.issuedcredit_note.created,identity_verification.cancelledidentity_verification.canceledandpayment.refund_recordedpayment.refunded, as the webhook documentation has always said. The feed used to show the internal name.actor_typeon the events of a member of staff or of StoreBay support working through the API now says so.payment.initiatedfor a card charge a staff token starts isstaff(it wasapi);credit.addedfor money recorded at the desk that leaves credit isstafforplatform_admin(it wasprovider); enquiry and plan-promotion events written in a StoreBay support session areplatform_admin(they werestafforapi).GET /v1/reports/delinquency:overdue_minorandoverdue_accountscount 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 soeconomic_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_minorandrevpau_minorare 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 onamount_minor, issuebelow_minimum: 30p for a card and £1.00 for Direct Debit in GBP. An offline recording has no minimum.POST /v1/orders/{id}/payrefuses 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 oncoupon_codefor a promotion that would leave a website order's first card payment at nothing or under that least.- A Bacs Direct Debit is
succeededwhen GoCardless confirms the collection, not when it is paid out. Its invoice is paid then,payment.succeededandinvoice.paidare sent then, andcharged_atis the confirmation;paid_outfollows days later and sends no event. A collection returned before the payout isfailed(payment.failed) and its invoice is reopened; one returned after it islate_failure_settledorchargeback_settled, as before. POST /v1/paymentswithrailbacs_ddagainst 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_datemoves with it. - An invoice's
statuscan go fromoverdueback toopen: 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 tooverdueif nothing has been collected once that day has passed. A client that readsoverdueas one-way should read the currentstatus. POST /v1/subscriptions/{id}/cancelbills 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}/voidputs the charges the invoice carried back on the plan's next invoice, unless the plan has ended.PATCH /v1/subscriptions/{id}: a newbilling_intervalorinterval_countre-prices every repeating charge and cover from the next invoice (the licence fee moves only by anamount_minorsent with it); a charge that cannot be priced for the new period refuses the whole change with 409conflict. On a plan with no licence fee, a change toamount_minoris a 409conflict, and anamount_minorequal to the current one is no change. On a pending plan, anext_billing_dateon or before the end of a period already invoiced is a 409conflict, and so is one while cover or a repeating charge starts on the currentnext_billing_dateor the agreement'smove_in_date: move themove_in_dateinstead. Clearingcollection_waivedor binding a card ends a standing order.PATCH /v1/agreements/{id}: amove_in_datechange moves the cover and repeating charges the plan was given from it, billed or not, and an explicitnullis a 409 while any starts on it. A differentunit_idmoves 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/agreementsnow appliesstatus,contact_idandsite_id, and answers 400 to a value that is not valid.POST /v1/dealswith nopipeline_idputs the deal on the operator's pipeline, and with nostage_idat that pipeline's first stage.quoted_atandreserved_atare 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}:statuswonorlostcloses the deal, moves it to a matching stage and clears its next call; a lost deal can be reopened withopen; a won deal'sstatuscannot change; a won or lost deal cannot be moved to an open stage; and the pipeline's won or lost stage sent withstatusopenis 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 namingagreement_id(422 without one); an API key or OAuth client is unchanged. Changingcontact_idon a deal linked to an agreement is a 422 onagreement_idwhen the agreement belongs to another customer. A body of onlynullvalues 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. Aoncecoupon discounts the licence fee only.POST /v1/credit-notesrefuses (409conflict) an invoice that bills insurance (ipt_minorabove 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 409cover_in_forcewhen customers hold the product and the change would alter what their cover is. A waiver whosenamesays insurance is a 422 onname, as are arate_per_thousand_minorabove 100000 and aregulatory_basis_refover 200 characters or with a non-printable one.billing_cycleismonthlyonly (weekly,annualandone_offare a 422).PATCH /v1/orders/{id}takes acoverage_product_idonly for an active fixed-level monthly waiver whose name does not say insurance (422), and an explicitnullremoves the cover. - Contacts:
marketing_consentfalseon create records nothing, and on update withdraws an earlier yes. Anocannot be set back tonot_asked(422). POST /v1/subscriptionswith noitemsmakes one licence-fee line at the unit type's VAT treatment. Acoverageitem is refused (422 onitems[i].item_type);coverageleaves that enum.POST /v1/messagesanswers 422 oncontact_idfor 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_typeisagreement,collections_case,contact,dealorunit(422 otherwise; existing notes keep theirs), and a contact subject that is the walk-in record is a 422 onsubject_id.- The walk-in record is refused as the customer on the
/v1writes for agreements, reservations, orders, deals, notes and messages (422), and it cannot be changed or archived (409). - Payments recorded with a staff token:
payment.succeededandinvoice.paidnamestaffas the actor, notapi. contact.unarchivedis also sent when an archived customer returns by enquiring, booking or being added again, andcontact.updatedwhen a customer saves their own details in the portal.- An order left
awaiting_paymentfor over a day with nothing in flight is ended asabandoned.
Fixed
POST /v1/agreementsno 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/dealsandPATCH /v1/deals/{id}check the customer, site, unit type and agreement they name against the caller's sites (403site_forbidden).- The event catalogue said
reservation.cancelledalso 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 sendsreservation.expired.
6 Jul 2026
Added
- The complete
/v1REST 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.*, andwebhook_endpoint.disabled), ~8-attempt exponential-backoff retries, manual redelivery, and auto-disable on sustained failure. OAuth2client-credentials and authorization-code + PKCE (for marketplace apps), plus therefresh_tokengrant.- 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.
Readiness probe GET
Unauthenticated **readiness** check (`GET /v1/ready`) — the deep sibling of `/health`, not a replacement for it. Where `/health` reports only that the process is serving, this one verifies the dependency every other endpoint needs. It pings the database inside a short timeout, then answers `503` (`status: degraded`) when it is unreachable and `200` (`status: ok`) otherwise. Use `/health` to ask "is the process alive?" (liveness — e.g. a restart decision) and `/ready` to ask "can it actually serve?" (readiness — e.g. load-balancer rotation or an uptime monitor). It requires no credential and returns no tenant data, but it is rate-limited per source IP, so poll it on the order of once a minute rather than continuously.
SDKs & client libraries
Thin, typed wrappers over the same /v1 API — same keys, scopes, and idempotency.