Build around
your studio.
Connect your own tools with the REST API, TypeScript SDK, and webhooks. Build on the same API that powers the studio console.
| Event | Endpoint | State | Tries | Answer |
|---|---|---|---|---|
| booking.created | hooks.example.com/soundslot | delivered | 1 | 200 |
| payment.succeeded | hooks.example.com/soundslot | delivered | 1 | 200 |
| series.occurrence_skipped | desk.example.com/bookings | delivered | 2 | 200 |
| booking.cancelled | desk.example.com/bookings | pending | 3 | 502 |
| payout.failed | desk.example.com/bookings | failed | 8 | no answer |
Start building.
One REST API for rooms, sessions, customers and money, a TypeScript SDK generated from it, and an event for anything that changes. The console and the storefront are clients of the same thing.
npm i @soundslot/sdk-publicnpx @soundslot/cli theme init my-themen8n-nodes-soundslotNot writing code? The Soundslot app for Zapier and the node for n8n carry every event as a trigger and customers and bookings as actions.
API access, webhooks and the SDK are on every plan
Follow every delivery.
The studio's own delivery log, in the console and on the API. What went out, how many attempts it took, and what the receiving server said.
Everything that happens, as it happens.
Register a URL, pick the events you want, and we deliver them. This is the whole list, read out of the same file the platform emits from — not a sample of it.
booking
- booking.created
- a booking was made (storefront or desk); a storefront booking is unpaid until payment.succeeded
- booking.updated
- time, room, or details changed
- booking.cancelled
- cancelled by the customer or the desk
- booking.approved
- an approval-room booking was confirmed; card captured
- booking.declined
- declined; the card hold was released
- booking.completed
- a session's time passed with nobody saying otherwise
- booking.no_show
- staff marked a session a no-show
- booking.abandoned
- a checkout was started and never paid; the hold lapsed
- booking.extended
- time was added to a running session
- booking.change_requested
- a customer asked to move an approval-room session
class
- class.published
- a class went on sale
- class.updated
- a class was edited — the room-time, the price or how many seats
payment
- payment.succeeded
- money in — booking paid
- payment.authorized
- card hold placed for an approval room
- payment.failed
- a charge or auto-charge failed
- payment.refunded
- refund issued (card or account credit)
- payment.recorded
- the desk recorded money taken outside the platform
- payment.disputed
- a cardholder disputed a charge
payout
- payout.failed
- a payout to your bank was rejected — the money is still in Stripe
series
- series.created
- a recurring weekly slot was set up
- series.updated
- series edited — applies forward only
- series.ended
- a recurring slot was ended
- series.change_requested
- a customer asked to change their recurring slot
- series.occurrence_skipped
- one week of a series was skipped
- series.occurrence_removed
- one week of a series was removed
- series.occurrence_restored
- a skipped week was restored
hour_pack
- hour_pack.purchased
- a customer bought an hour pack; account credit granted
gift_card
- gift_card.purchased
- somebody bought a gift card; the code is now live
- gift_card.redeemed
- a gift card was handed in; credit granted
review
- review.created
- a customer rated a session; it may be published or waiting
rental
- rental.reserved
- gear was reserved
- rental.overdue
- rented gear was not returned on time
tenancy
- tenancy.created
- a monthly lockout was set up
- tenancy.charged
- a monthly lockout's rent was collected
- tenancy.charge_failed
- a monthly lockout's rent charge failed
- tenancy.ended
- a monthly lockout ended
membership
- membership.started
- a customer joined a membership plan
- membership.charged
- a membership's monthly fee was collected
- membership.charge_failed
- a membership's monthly charge failed
- membership.ended
- a membership was cancelled or ran out of retries
flow
- flow.staff_notification
- a flow asked to notify staff
One delivery, and this is the whole body.
Every event arrives as the same four keys: the delivery id, the type, when it fired, and the data for that type. A booking carries the booking, priced and settled, so a receiver needs no second call to make sense of it.
- webhook-id
- The delivery id. Stable across retries, so it is your dedupe key.
- webhook-timestamp
- Unix seconds, signed, so a captured request cannot be replayed at you later.
- webhook-signature
- HMAC over id.timestamp.body with the whsec_ secret shown once at registration.
Verify with any Standard Webhooks library, and verify before you parse.
{
"id": "7f0a1c6e-3d21-4a8f-9c0b-2ab55e9d1042",
"type": "booking.created",
"timestamp": "2026-09-07T18:04:11.216Z",
"data": {
"booking": {
"id": "b1f4c0d2-8e77-4b19-9d3a-51c6f2a7e884",
"roomId": "3c9a7e21-45bd-4f10-8a62-9d7c1b40e553",
"locationId": "0c2f8d31-77ae-4c05-93b1-6e4a2d8f1c90",
"customerId": "5a83be04-1c6d-4e77-b219-8f0c3a6d4b21",
"activityId": null,
"staffId": null,
"kind": "session",
"customFields": {},
"seriesId": null,
"bandId": null,
"startsAt": "2026-09-08T18:00:00.000Z",
"endsAt": "2026-09-08T20:00:00.000Z",
"status": "confirmed",
"subtotalCents": 4000,
"taxCents": 0,
"totalCents": 4000,
"creditAppliedCents": 0,
"paymentStatus": "paid",
"paymentPlan": "full",
"depositCents": null,
"tipCents": 500,
"bandName": null,
"discountCents": 0,
"promotionCode": null,
"balance": null,
"offlinePaymentMethod": null,
"offlinePaymentNote": null,
"paidAt": "2026-09-07T18:04:10.884Z",
"notes": null,
"source": "storefront",
"cancelledAt": null,
"cancelReason": null,
"checkedInAt": null,
"createdAt": "2026-09-07T18:04:10.612Z"
}
}
}Your server was down. Ours kept trying.
Each event is sent up to 8 times, the wait doubling after every refusal. Every attempt is on the record, and nothing needs polling.
| Attempt | Wait | After the event |
|---|---|---|
| 1 | sent | — |
| 2 | 30s | 30s |
| 3 | 1m | 1m 30s |
| 4 | 2m | 3m 30s |
| 5 | 4m | 7m 30s |
| 6 | 8m | 15m 30s |
| 7 | 16m | 31m 30s |
| 8 | 32m | 63m 30s |
- Answer 2xx and get on with it
- Store the delivery, answer, then do the work. Anything else — or no answer at all — is retried.
- A failure keeps the evidence
- Your status code and the first 2,000 characters of your response are stored against the delivery. A request that got no answer records why instead: DNS, refused, an expired certificate.
- The last attempt closes it
- The delivery is marked failed and stays visible. GET /api/v1/admin/webhook-deliveries lists them newest first, filterable by state and by endpoint.
- A replay is a new delivery
- It carries the same payload and its own webhook-id, so a receiver deduping on that header will process it again. That is the point of a replay.
The console is a client of the API.
There is no smaller read-only mirror to drift from the real thing. Availability is worked out live — opening hours, minus blackouts, minus what is booked, with minimums and turnover buffers already applied — so what the API hands back is what can be booked.
import { client } from "@soundslot/sdk-public/client";
import { adminCreateBooking, storefrontGetAvailability } from "@soundslot/sdk-public";
client.setConfig({
headers: { Authorization: `Bearer ${process.env.SOUNDSLOT_API_KEY}` },
});
// what can actually be booked in this room on this day, in the location's zone
const day = await storefrontGetAvailability({
path: { slug: "riff-city" },
query: { roomId, date: "2026-09-14" },
});
await adminCreateBooking({
body: { roomId, customerId, startsAt, endsAt, payment: "send_link" },
});Sections of the reference
- Recording projects
- Bookings
- Series
- Classes
- Rooms
- Catalog
- Customers
- Staff
- Roles
- Rentals
- Revenue
- Reports
- Messaging
- Flows
- Billing
- Payments
- Studio
- Themes
- Media
- Documents
- Tenancies
- Developers
- Storefront
- Checkout
- Calendar
- Webhooks
The public document, and what the TypeScript SDK is generated from. Rendered at /api/docs, with every webhook event and its exact body at the bottom.
The same registry with the platform control plane still in it. Gated to platform admins, never published. The public one is derived from it by dropping those operations and pruning any schema they alone referenced.
- bearerAuthAuthorization: Bearer ssk_…
- An API key, minted in the console under Developers and shown once. It acts as the person who issued it, so every permission and tenant check runs unchanged.
- staffSession__Host-staff-auth.session_token
- The console's own cookie. Browser flows only — an integration uses a key.
- customerSession__Host-customer-auth.session_token
- A storefront customer. Browsing needs none; their account, saved cards and receipts do.
Build the storefront itself.
A theme is a folder of Liquid, JSON and CSS in the shape Shopify developers already know: a layout, sections with schemas, JSON templates and a settings schema. Start from the reference theme with npx @soundslot/cli theme init my-theme, then push it and a studio switches to it.
Most of this is a Zap, with no code to write.
Point Webhooks by Zapier, Make or n8n at your studio and the payload above is the trigger. These six are afternoons, not projects.
- A booking is confirmed
- Drop it on the shared studio calendar the whole team already watches
- Google Calendar · Outlook
- Someone cancels
- Ping the desk, clear the calendar, and text whoever wanted that slot
- Slack · Twilio · Google Calendar
- A payment lands
- Append a row your bookkeeper can reconcile at month end
- Google Sheets · Airtable
- A new customer signs up
- Add them to the mailing list, tagged by the room they booked
- Mailchimp · Klaviyo
- A card fails on a weekly slot
- Tell you before the band turns up, not after
- Slack · email · SMS
- A payout to your bank is rejected
- Wake somebody up — the money is still sitting in Stripe until you fix it
- Slack · SMS · PagerDuty