Avada Appointment Booking
REST API

REST API

Everything an assistant can do through MCP is also a plain HTTP request, authenticated with the same MCP key. Use this if you are writing a script, a scheduled job, or an agent runtime of your own.

If you just want to connect an off-the-shelf assistant, you do not need this page — see MCP keys or Run the connector on your machine.

Base URL

https://sea-appointment-booking.firebaseapp.com/rest_api/v2

🔑 Authenticating

Send your key as a bearer token. Nothing else identifies the store — the key already belongs to one.

curl https://sea-appointment-booking.firebaseapp.com/rest_api/v2/services \
  -H "Authorization: Bearer mcp_your-key-here"

Access tokens issued to an assistant that signed in with Shopify work here too, with the permissions granted on the approval screen.

📦 Responses

Every answer uses the same envelope.

{"success": true, "data": [...], "meta": {"count": 12}}
{"success": false, "error": "Service not found", "code": "NOT_FOUND"}

code is stable and safe to branch on; error is a sentence meant to be read.

StatuscodeWhat to do
401AUTH_FAILEDThe key is wrong or was deleted. Do not retry
401KEY_EXPIREDCreate a new key under Settings > Developers
403SCOPE_DENIEDThe key lacks that permission — the message names it. Do not retry
404NOT_FOUNDWrong id, or the record belongs to another store
400VALIDATION_FAILEDFix the request body; the same call will fail again unchanged
429RATE_LIMITED60 requests per minute per key. Wait for Retry-After

🪪 What this key can do

GET /whoami

Returns the store, the permissions on the key, and fieldGroups — the map of which fields each edit permission may write. Read it once at startup rather than hard-coding field lists.

{
  "success": true,
  "data": {
    "shopId": "…", "shopDomain": "demo.myshopify.com",
    "keyName": "Support agent", "active": true,
    "read": ["service", "appointment"],
    "actions": ["service.edit_general"],
    "fieldGroups": {"service": {"service.edit_general": ["price", "status", "…"]}, "…": {}}
  }
}

👀 Reading

EndpointPermissionNotes
GET /dashboard/statsDashboardstartDate, endDate optional — defaults to the last 30 days
GET /servicesServicesearchText, status, limit (1–100, default 20)
GET /services/:idService
GET /packagesPackagesearchText, status, limit
GET /packages/:idPackage
GET /appointmentsAppointmentstatus, paymentStatus, serviceId, staffId, startDate, endDate, limit
GET /appointments/:idAppointment
GET /staffs · GET /staffs/:idStaff
GET /customers · GET /customers/:idCustomer
GET /customers/:id/appointmentsCustomerBooking history for one customer
GET /settingsSettingSMTP credentials are never included
GET /integrations/:providerManage integrationsconnected: false rather than 404 when nothing is connected

✍️ Writing

EndpointPermission
POST /servicesCreate service
PUT /services/:idAny Service edit permission
DELETE /services — body {"ids": ["…"]}Delete service
POST /packagesCreate package
PUT /packages/:idAny Package edit permission
DELETE /packages/:idDelete package
POST /appointmentsCreate appointment
PUT /appointments/:idEdit appointment
POST /appointments/:id/rescheduleReschedule / cancel
PUT /settingsAny Setting edit permission
DELETE /integrations/:providerManage integrations

One endpoint, tab-shaped permissions

There is a single PUT per record, but permissions are granted per edit tab. A request only writes the fields the key's permissions cover; anything else is dropped and named back to you:

{
  "updated": true,
  "data": {"...": "..."},
  "ignoredFields": ["timeSlots"],
  "note": "Fields outside this permission were ignored."
}

So a key holding only Service > General can send price and timeSlots together — the price changes, availability does not, and ignoredFields says so. Never assume a write landed because the call returned 200; check ignoredFields.

GET /whoamifieldGroups tells you which fields a given permission covers, so you can avoid sending fields that will be dropped.

Every POST that creates a record — services, packages and appointments alike — answers in this same shape, so the record you just made arrives under data and anything the endpoint would not accept is listed beside it rather than dropped in silence.

⚠️

Some fields are never writable, whatever the key holds — package totals, money and order links on appointments, record identity, SMTP credentials. They appear in ignoredFields too. See What each permission unlocks.

🕐 Dates and times

Dates and times are wall-clock, never UTC. On POST /appointments they are read in timezone — the customer's — which defaults to the store's when you leave it out; read the store's own from GET /settings. The booking is stored in both frames, so a customer abroad is booked in one call.

On POST /appointments/:id/reschedule the same customer timezone is called clientTimezone, and the fields carrying the new slot are shopDayStart / shopTimeStart / shopTimeEnd — they are read in clientTimezone despite the shop prefix. A timezone sent to that endpoint is the store's own, which the app fills in itself: it comes back in ignoredFields rather than being written.

Send all three slot fields or none. A reschedule naming only part of a slot answers 400 saying which are missing: a partial slot used to rewrite the time shown on the booking while leaving the booking itself where it was, so the calendar, the reminder email and the widget all disagreed with the record.

curl -X POST https://sea-appointment-booking.firebaseapp.com/rest_api/v2/appointments \
  -H "Authorization: Bearer mcp_your-key-here" \
  -H "Content-Type: application/json" \
  -d '{
        "serviceId": "svc_123",
        "timezone": "Asia/Bangkok",
        "date": "2026-09-01",
        "startTime": "10:00",
        "customerEmail": "[email protected]",
        "customerName": "Alex"
      }'

Price, duration and staff come from the service, so only the slot and the customer are needed. A service that runs at a store branch needs locationId too once the store has more than one — the call answers 400 listing the branches to choose from. Every new booking is written APPROVED and UNPAID; status and paymentStatus sent to this endpoint land in ignoredFields, and moving either afterwards is PUT /appointments/:id under the Edit appointment permission.

The slot itself has to be one the store would sell: in the future, past the service's minimum notice, and inside its booking window. One that is not answers 400 VALIDATION_FAILED naming which of the three it broke, on this endpoint and on reschedule alike.

Changelog

Changes to this API are listed here, newest first. A field that stops being accepted comes back in ignoredFields rather than erroring, so a caller that never reads it sees only silence.

2026-08 — appointment writes

This is the shape v2 launches with. The entries below record what moved during the preview, for anyone who wrote against a staging build before release — none of them break a published caller, because there was not one yet.

  • POST /appointments accepts timezone (the customer's) and locationId (the branch). Dates and times were previously read in the store's timezone with no way to say otherwise.
  • POST /appointments no longer accepts status or paymentStatus. Every booking is created APPROVED and UNPAID; both now appear in ignoredFields. Use PUT /appointments/:id.
  • POST /appointments/:id/reschedule no longer accepts timezone — the store's timezone is the store's to state, and it now appears in ignoredFields. clientTimezone is unchanged and is still how you name the customer's.
  • POST /appointments/:id/reschedule requires the whole slot — shopDayStart, shopTimeStart and shopTimeEnd together — or none of it. Sending part of one now answers 400; it previously wrote the new wall-clock without moving the booking.
  • Cancelling a paid booking now requires refundCustomer. Cancellation is the only moment a refund can be issued from the app, so leaving it unsaid answers 400 instead of quietly keeping the money.
  • POST /appointments answers {"updated": true, "data": {…}}, the same shape as POST /services and POST /packages — so the booking is under data.data, not at the top level. The three create endpoints had drifted apart during the preview; this is what makes them agree.
  • Bookings are held to the service's own booking window — past dates, its booking range and its minimum notice — on both endpoints. These rules ran only in the storefront's slot grid before.

Limits

  • 60 requests per minute per key. Beyond that you get 429 with Retry-After.
  • A key reaches exactly one store — there is no cross-store call, and an id from another store answers 404.
  • Deleting or regenerating a key cuts off every caller using it immediately.
Products
Avada SEO SuiteAvada AEO OptimizerAvada AI Blog BuilderAvada Product CopyAvada Images & Page Speed UpAvada Shipping LabelsAvada Backups & Restore
Resources
DocumentationSEO Suite DocsBlog DocsSpeed DocsShipping Labels DocsBackups & Restore Docs
Company
Avada GroupPrivacy Policy
© 2026 Avada Group. All rights reserved.