Appointments API
Every Appointments action (26) and data model (6): endpoints, scopes, input, preconditions, events and examples.
Online booking that respects your hours, breaks, time off and capacity.
Module code appointments · 26 actions · 6 data models · 20 events. A key or token never does more here than the person behind it can; row-level security applies exactly as in the app. Scopes: appointments (everything), appointments:read (read only) or appointments:<capability>.
Resources
| Resource | Table | List and read | POST | PATCH | DELETE |
|---|---|---|---|---|---|
appointment_guest_services | appointment_guest_service | /api/v1/appointment_guest_services | |||
appointment_guests | appointment_guest | /api/v1/appointment_guests | |||
appointment_import_jobs | appointment_import_job | /api/v1/appointment_import_jobs | |||
appointment_reminder_opt_outs | appointment_reminder_opt_out | /api/v1/appointment_reminder_opt_outs | |||
appointments | appointment | /api/v1/appointments | appointments.book | ||
banned_customers | banned_customer | /api/v1/banned_customers |
Lists take limit (up to 200), cursor, order (e.g. -created_at), fields and q; every other query parameter is a column filter: ?status=paid, ?total[gte]=100, ?id[in]=a,b, ?x=null.
Actions
| Action | Needs | What it does |
|---|---|---|
appointments.add_guest | appointments:write | Add a guest |
appointments.add_guest_service | appointments:write | Add a service |
appointments.assign_member | appointments:write | Change who does it |
appointments.ban_customer | appointments:manage | Ban from booking |
appointments.book | appointments:write | Book an appointment |
appointments.book_group | appointments:write | Book an appointment |
appointments.book_online | appointments:write | Book online |
appointments.cancel | appointments:write | Cancel an appointment |
appointments.cancel_by_token | appointments:write | Cancel by link |
appointments.cancel_my_booking | appointments:write | Cancel my booking |
appointments.change_guest_customer | appointments:write | Change the guest |
appointments.complete | appointments:write | Mark as completed |
appointments.confirm | appointments:approve | Confirm an appointment |
appointments.export | appointments:export | Export appointments |
appointments.import | appointments:import | Import appointments |
appointments.lift_ban | appointments:manage | Lift a ban |
appointments.mark_deposit_status | appointments:handle | Update booking deposit |
appointments.no_show | appointments:write | Mark as no-show |
appointments.raise_reminder | appointments:handle | Raise a booking reminder |
appointments.remove_guest | appointments:write | Remove a guest |
appointments.remove_service | appointments:write | Remove a service |
appointments.reschedule | appointments:write | Move the appointment |
appointments.reschedule_by_token | appointments:write | Move by link |
appointments.reschedule_my_booking | appointments:write | Move my booking |
appointments.set_reminder_opt_out | appointments:write | Booking reminders for a person |
appointments.update_details | appointments:write | Edit the note |
appointments.add_guest
Add a guest — Puts another person on an existing appointment.
Needs appointments:write · changes data
https://app.getera.io/api/v1/appointments/{id}/actions/add_guest- Canonical address:
POST /api/v1/actions/appointments.add_guest
Input
| Field | Type | Required | Notes |
|---|---|---|---|
appointmentId | uuid | yes | Filled from the path {id} · Appointment · id of appointment |
customerId | uuid | yes | Customer · id of customer |
itemVariantId | uuid | Their first service · id of item_variant | |
memberUserId | uuid | Team member · Leave empty to assign whoever is free. | |
expectedPriceAmount | number (money) | Price shown · The price the screen showed. A different current price is refused so the person can agree to it. · min 0 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The appointment must exist. |
already_cancelled | The appointment must not be cancelled. |
already_finished | The appointment must not be finished. |
already_a_guest | The person must not already be on it. |
customer_not_found | The customer must exist in this workspace. |
customer_banned | The customer must not be banned. |
service_not_found | The service must exist. |
service_inactive | The service must be active and bookable. |
service_has_no_duration | A non-package service must have a duration. |
package_empty | A package must contain something. |
service_not_offered_that_day | The service must be offered on that weekday. |
provider_cannot_perform | The team member must perform this service. |
closed_that_day | The date must not be closed by an override. |
outside_working_hours | The longer appointment must still fit in opening hours. |
provider_on_leave | Nobody needed must be on leave. |
slot_taken | The extended slot must be free. |
class_full | A class must still have room. |
no_provider_available | Someone must be free when none was chosen. |
price_changed | The price must still be the one the person was shown. |
Events it publishes: appointments.guest_added
What it writes: appointment_guest (insert) — Adds the guest; appointment_guest_service (insert) — Adds their first service line; appointment (update) — Re-totals duration, price and end time
curl -X POST https://app.getera.io/api/v1/appointments/APPOINTMENT_ID/actions/add_guest \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"customerId":"CUSTOMER_ID"}'{
"ok": true,
"result": "…",
"events": [
"appointments.guest_added"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointments.addGuest("APPOINTMENT_ID", {
customerId: "CUSTOMER_ID"
})era appointments add_guest APPOINTMENT_ID \
--customer-id "CUSTOMER_ID"{
"name": "era__appointments__add_guest",
"arguments": {
"appointmentId": "APPOINTMENT_ID",
"customerId": "CUSTOMER_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, already_cancelled, already_finished, already_a_guest, customer_not_found, customer_banned, service_not_found, service_inactive, service_has_no_duration, package_empty, service_not_offered_that_day, provider_cannot_perform, closed_that_day, outside_working_hours, provider_on_leave, slot_taken, class_full, no_provider_available, price_changed · 429 slow down
appointments.add_guest_service
Add a service — Adds one more service for a guest, re-checking the longer appointment.
Needs appointments:write · changes data
https://app.getera.io/api/v1/appointment_guest_services/actions/add_guest_service- Canonical address:
POST /api/v1/actions/appointments.add_guest_service
Input
| Field | Type | Required | Notes |
|---|---|---|---|
appointmentGuestId | uuid | yes | Guest · id of appointment_guest |
itemVariantId | uuid | yes | Service · id of item_variant |
memberUserId | uuid | Team member · Leave empty to assign whoever is free. | |
expectedPriceAmount | number (money) | Price shown · The price the screen showed. A different current price is refused so the person can agree to it. · min 0 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
guest_not_found | The guest must be on an appointment. |
price_changed | The price must still be the one the person was shown. |
already_cancelled | The appointment must not be cancelled. |
already_finished | The appointment must not be finished. |
service_inactive | The service must be active and bookable. |
service_has_no_duration | A non-package service must have a duration. |
package_empty | A package must contain something. |
service_not_offered_that_day | The service must be offered on that weekday. |
provider_cannot_perform | The team member must perform this service. |
closed_that_day | The date must not be closed by an override. |
outside_working_hours | The longer appointment must still fit in opening hours. |
provider_on_leave | Nobody needed must be on leave. |
slot_taken | The extended slot must be free. |
class_full | A class must still have room. |
no_provider_available | Someone must be free when none was chosen. |
Events it publishes: appointments.service_added
What it writes: appointment_guest_service (insert) — Adds the service line; appointment (update) — Re-totals duration, price and end time
curl -X POST https://app.getera.io/api/v1/appointment_guest_services/actions/add_guest_service \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"appointmentGuestId":"APPOINTMENT_GUEST_ID","itemVariantId":"ITEM_VARIANT_ID"}'{
"ok": true,
"result": "…",
"events": [
"appointments.service_added"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointmentGuestServices.addGuestService({
appointmentGuestId: "APPOINTMENT_GUEST_ID",
itemVariantId: "ITEM_VARIANT_ID"
})era appointment_guest_services add_guest_service \
--appointment-guest-id "APPOINTMENT_GUEST_ID" \
--item-variant-id "ITEM_VARIANT_ID"{
"name": "era__appointments__add_guest_service",
"arguments": {
"appointmentGuestId": "APPOINTMENT_GUEST_ID",
"itemVariantId": "ITEM_VARIANT_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 guest_not_found, price_changed, already_cancelled, already_finished, service_inactive, service_has_no_duration, package_empty, service_not_offered_that_day, provider_cannot_perform, closed_that_day, outside_working_hours, provider_on_leave, slot_taken, class_full, no_provider_available · 429 slow down
appointments.assign_member
Change who does it — Moves one service line to another team member, or leaves it unassigned.
Needs appointments:write · changes data
https://app.getera.io/api/v1/appointment_guest_services/{id}/actions/assign_member- Canonical address:
POST /api/v1/actions/appointments.assign_member
Input
| Field | Type | Required | Notes |
|---|---|---|---|
lineId | uuid | yes | Filled from the path {id} · Service line · id of appointment_guest_service |
memberUserId | uuid | Team member · Leave empty to unassign. |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
line_not_found | The service line must exist. |
already_cancelled | The appointment must not be cancelled. |
already_finished | The appointment must not be finished. |
provider_cannot_perform | The team member must perform this service. |
closed_that_day | The day must not be closed for them. |
outside_working_hours | They must be working across the appointment. |
provider_on_leave | They must not be on leave. |
slot_taken | They must be free for the whole appointment. |
Events it publishes: appointments.member_assigned
What it writes: appointment_guest_service (update) — Sets the line’s team member
curl -X POST https://app.getera.io/api/v1/appointment_guest_services/APPOINTMENT_GUEST_SERVICE_ID/actions/assign_member \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"memberUserId":"MEMBER_USER_ID"}'{
"ok": true,
"result": "…",
"events": [
"appointments.member_assigned"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointmentGuestServices.assignMember("APPOINTMENT_GUEST_SERVICE_ID", {
memberUserId: "MEMBER_USER_ID"
})era appointment_guest_services assign_member APPOINTMENT_GUEST_SERVICE_ID \
--member-user-id "MEMBER_USER_ID"{
"name": "era__appointments__assign_member",
"arguments": {
"lineId": "APPOINTMENT_GUEST_SERVICE_ID",
"memberUserId": "MEMBER_USER_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 line_not_found, already_cancelled, already_finished, provider_cannot_perform, closed_that_day, outside_working_hours, provider_on_leave, slot_taken · 429 slow down
appointments.ban_customer
Ban from booking — Stops an email address or phone number from making new bookings.
Needs appointments:manage · reaches outside (sends, publishes or charges) · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/banned_customers/actions/ban_customer- Canonical address:
POST /api/v1/actions/appointments.ban_customer
Input
| Field | Type | Required | Notes |
|---|---|---|---|
email | Email · max 255 | ||
phone | phone | Phone · max 32 | |
reason | string (enum) | Reason · Allowed values: fake_appointment, no_show, inappropriate, payment_issue, other | |
notes | string | Notes · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
no_identity | An email address or a phone number is needed. |
Events it publishes: appointments.customer_banned
What it writes: banned_customer (insert) — Records the ban
curl -X POST https://app.getera.io/api/v1/banned_customers/actions/ban_customer \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"email":"[email protected]"}'{
"ok": true,
"result": "…",
"events": [
"appointments.customer_banned"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.bannedCustomers.banCustomer({
email: "[email protected]"
})era banned_customers ban_customer \
--email "[email protected]"{
"name": "era__appointments__ban_customer",
"arguments": {
"email": "[email protected]"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 no_identity · 429 slow down
appointments.book
Book an appointment — Opens an appointment with its first guest and service, once every rule allows it.
Needs appointments:write · changes data
https://app.getera.io/api/v1/appointments/actions/book- REST alias:
POST /api/v1/appointments - Canonical address:
POST /api/v1/actions/appointments.book
Input
| Field | Type | Required | Notes |
|---|---|---|---|
customerId | uuid | yes | Customer · id of customer |
itemVariantId | uuid | yes | Service · id of item_variant |
appointmentDate | date | yes | Date |
startTime | string | yes | Start time · max 8 |
memberUserId | uuid | Team member · Leave empty to assign whoever is free. | |
notes | string | Note · max 2000 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
service_inactive | The service must be active and bookable. |
service_has_no_duration | A non-package service must have a duration. |
package_empty | A package must contain something. |
inside_cutoff | The time must respect the booking cutoff. |
outside_window | The time must fall inside the booking window. |
service_not_offered_that_day | The service must be offered on that weekday. |
provider_cannot_perform | The team member must perform this service. |
closed_that_day | The date must not be closed by an override. |
outside_working_hours | The slot and its buffer must fall inside opening hours. |
provider_on_leave | Nobody needed must be on leave. |
slot_taken | The slot, plus its buffer, must be free. |
class_full | A class must still have room. |
no_provider_available | Someone must be free when none was chosen. |
customer_not_found | The customer must exist in this workspace. |
customer_banned | The customer must not be banned. |
no_workspace | The workspace must be readable, for its clock. |
Events it publishes: appointments.booked
What it writes: appointment (insert) — Creates the appointment; appointment_guest (insert) — Adds the first guest; appointment_guest_service (insert) — Adds their first service
curl -X POST https://app.getera.io/api/v1/appointments/actions/book \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"customerId":"CUSTOMER_ID","itemVariantId":"ITEM_VARIANT_ID","appointmentDate":"2026-11-02","startTime":"Start time"}'{
"ok": true,
"result": "…",
"events": [
"appointments.booked"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointments.create({
customerId: "CUSTOMER_ID",
itemVariantId: "ITEM_VARIANT_ID",
appointmentDate: "2026-11-02",
startTime: "Start time"
})era appointments create \
--customer-id "CUSTOMER_ID" \
--item-variant-id "ITEM_VARIANT_ID" \
--appointment-date "2026-11-02" \
--start-time "Start time"{
"name": "era__appointments__book",
"arguments": {
"customerId": "CUSTOMER_ID",
"itemVariantId": "ITEM_VARIANT_ID",
"appointmentDate": "2026-11-02",
"startTime": "Start time"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 service_inactive, service_has_no_duration, package_empty, inside_cutoff, outside_window, service_not_offered_that_day, provider_cannot_perform, closed_that_day, outside_working_hours, provider_on_leave, slot_taken, class_full, no_provider_available, customer_not_found, customer_banned, no_workspace · 429 slow down
appointments.book_group
Book an appointment — Writes a whole booking — every guest, every service, every staff member — in one transaction.
Needs appointments:write · changes data
https://app.getera.io/api/v1/appointments/actions/book_group- Canonical address:
POST /api/v1/actions/appointments.book_group
Input
| Field | Type | Required | Notes |
|---|---|---|---|
guests | object / array | yes | Guests and their services · [{ customerId, lines: [{ itemVariantId, memberUserId? }] }] |
appointmentDate | date | yes | Date |
startTime | string | yes | Start time · max 8 |
notes | string | Note · max 2000 | |
address | string | Address · Home service only — where the visit happens. · max 500 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
invalid_guests | The guest list must name a customer and at least one service each. |
service_not_found | Every service must exist. |
service_inactive | Every service must be active and bookable. |
service_has_no_duration | A non-package service must have a duration. |
package_empty | A package must contain something. |
inside_cutoff | The time must respect the booking cutoff. |
outside_window | The time must fall inside the booking window. |
service_not_offered_that_day | Every service must be offered on that weekday. |
provider_cannot_perform | Each named team member must perform the service given to them. |
closed_that_day | The date must not be closed by an override. |
outside_working_hours | The whole booking, plus its buffer, must fall inside opening hours. |
provider_on_leave | Nobody needed may be on leave. |
slot_taken | Every named member must be free for the whole booking. |
class_full | A class must still have room. |
no_provider_available | Someone must be free for each service where none was chosen. |
already_a_guest | The same person must not be listed twice. |
customer_not_found | Every guest must exist in this workspace. |
customer_banned | No guest may be banned. |
no_workspace | The workspace must be readable, for its clock. |
Events it publishes: appointments.booked
What it writes: appointment (insert) — Creates the appointment; appointment_guest (insert) — Adds every guest; appointment_guest_service (insert) — Adds every service line
curl -X POST https://app.getera.io/api/v1/appointments/actions/book_group \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"guests":{},"appointmentDate":"2026-11-02","startTime":"Start time"}'{
"ok": true,
"result": "…",
"events": [
"appointments.booked"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointments.bookGroup({
guests: {},
appointmentDate: "2026-11-02",
startTime: "Start time"
})era appointments book_group --data '{"guests":{},"appointmentDate":"2026-11-02","startTime":"Start time"}'{
"name": "era__appointments__book_group",
"arguments": {
"guests": {},
"appointmentDate": "2026-11-02",
"startTime": "Start time"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 invalid_guests, service_not_found, service_inactive, service_has_no_duration, package_empty, inside_cutoff, outside_window, service_not_offered_that_day, provider_cannot_perform, closed_that_day, outside_working_hours, provider_on_leave, slot_taken, class_full, no_provider_available, already_a_guest, customer_not_found, customer_banned, no_workspace · 429 slow down
appointments.book_online
Book online — A visitor books one or more services at a free time from the site, under every rule the team’s booking applies, and gets a link to manage it.
Needs appointments:write · changes data · Also callable from a published site (visitor)
/api/site/v1/{siteId}, e.g. with @getera/storefront). With an API key or a token it is refused (403 site_only).https://app.getera.io/api/v1/appointments/actions/book_online- Canonical address:
POST /api/v1/actions/appointments.book_online
Input
| Field | Type | Required | Notes |
|---|---|---|---|
itemVariantIds | object / array | yes | Services |
appointmentDate | date | yes | Date |
startTime | string | yes | Start time · max 8 |
memberUserId | uuid | Team member · Empty for anyone who is free. | |
notes | string | Note · max 1000 | |
address | string | Address · Where a home-service visit happens. · max 500 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
address_required | A home-service booking needs the address the visit goes to. |
guest_details_required | A guest leaves their details before booking. |
service_not_found | At least one service must be chosen. |
too_many_services | At most five services on one online booking. |
slot_taken | The time must still be free when the booking is written. |
Events it publishes: appointments.booked, appointments.service_added
What it writes: appointment (insert) — Creates the appointment with its manage link; appointment_guest (insert) — The person booking; appointment_guest_service (insert) — One line per service
Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 address_required, guest_details_required, service_not_found, too_many_services, slot_taken · 429 slow down
appointments.cancel
Cancel an appointment — Frees the slot and tells the rest of the system it is free.
Needs appointments:write · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/appointments/{id}/actions/cancel- Canonical address:
POST /api/v1/actions/appointments.cancel
Input
| Field | Type | Required | Notes |
|---|---|---|---|
appointmentId | uuid | yes | Filled from the path {id} · Appointment · id of appointment |
reason | string | Reason · One of the workspace’s cancellation reasons. · max 64 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
already_cancelled | It must not already be cancelled. |
already_completed | A completed appointment cannot be cancelled. |
unknown_reason | The reason must be one the workspace uses. |
Events it publishes: appointments.cancelled
What it writes: appointment (update) — Marks it cancelled and records why
curl -X POST https://app.getera.io/api/v1/appointments/APPOINTMENT_ID/actions/cancel \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"reason":"Reason"}'{
"ok": true,
"result": "…",
"events": [
"appointments.cancelled"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointments.cancel("APPOINTMENT_ID", {
reason: "Reason"
})era appointments cancel APPOINTMENT_ID \
--reason "Reason"{
"name": "era__appointments__cancel",
"arguments": {
"appointmentId": "APPOINTMENT_ID",
"reason": "Reason"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 already_cancelled, already_completed, unknown_reason · 429 slow down
appointments.cancel_by_token
Cancel by link — Whoever holds a booking’s manage link cancels it, before the booking cutoff; the time is freed for others.
Needs appointments:write · removes or cannot be undone · asks a person to confirm when Era runs it · Also callable from a published site (visitor)
/api/site/v1/{siteId}, e.g. with @getera/storefront). With an API key or a token it is refused (403 site_only).https://app.getera.io/api/v1/appointments/actions/cancel_by_token- Canonical address:
POST /api/v1/actions/appointments.cancel_by_token
Input
| Field | Type | Required | Notes |
|---|---|---|---|
token | string | yes | Link · max 100 |
reason | string | Reason · max 80 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
booking_link_invalid | The link must name a booking of this business. |
booking_started | Not once it has started. |
inside_cutoff | Not inside the booking cutoff — the business is called instead. |
already_cancelled | It must not already be cancelled. |
Events it publishes: appointments.cancelled
What it writes: appointment (update) — Marks it cancelled
Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 booking_link_invalid, booking_started, inside_cutoff, already_cancelled · 429 slow down
appointments.cancel_my_booking
Cancel my booking — A signed-in customer cancels one of their own upcoming bookings from their site account; the slot is freed.
Needs appointments:write · removes or cannot be undone · asks a person to confirm when Era runs it · Also callable from a published site (customer)
/api/site/v1/{siteId}, e.g. with @getera/storefront). With an API key or a token it is refused (403 site_only).https://app.getera.io/api/v1/appointments/{id}/actions/cancel_my_booking- Canonical address:
POST /api/v1/actions/appointments.cancel_my_booking
Input
| Field | Type | Required | Notes |
|---|---|---|---|
appointmentId | uuid | yes | Filled from the path {id} · Booking · id of appointment |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_your_booking | Only a booking the customer is a guest on. |
booking_started | Not once it has started. |
already_cancelled | It must not already be cancelled. |
Events it publishes: appointments.cancelled
What it writes: appointment (update) — Marks it cancelled
Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_your_booking, booking_started, already_cancelled · 429 slow down
appointments.change_guest_customer
Change the guest — Puts a different person on a guest’s place, keeping their services, staff and prices.
Needs appointments:write · changes data
https://app.getera.io/api/v1/appointment_guests/{id}/actions/change_guest_customer- Canonical address:
POST /api/v1/actions/appointments.change_guest_customer
Input
| Field | Type | Required | Notes |
|---|---|---|---|
appointmentGuestId | uuid | yes | Filled from the path {id} · Guest · id of appointment_guest |
customerId | uuid | yes | Customer · id of customer |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
guest_not_found | The guest must be on an appointment. |
already_cancelled | The appointment must not be cancelled. |
already_finished | The appointment must not be finished. |
same_customer | The new person must be a different person. |
customer_not_found | The new person must exist. |
customer_banned | The new person must not be banned. |
already_a_guest | The new person must not already be on the booking. |
Events it publishes: appointments.guest_customer_changed
What it writes: appointment_guest (update) — Points the guest at the new customer
curl -X POST https://app.getera.io/api/v1/appointment_guests/APPOINTMENT_GUEST_ID/actions/change_guest_customer \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"customerId":"CUSTOMER_ID"}'{
"ok": true,
"result": "…",
"events": [
"appointments.guest_customer_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointmentGuests.changeGuestCustomer("APPOINTMENT_GUEST_ID", {
customerId: "CUSTOMER_ID"
})era appointment_guests change_guest_customer APPOINTMENT_GUEST_ID \
--customer-id "CUSTOMER_ID"{
"name": "era__appointments__change_guest_customer",
"arguments": {
"appointmentGuestId": "APPOINTMENT_GUEST_ID",
"customerId": "CUSTOMER_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 guest_not_found, already_cancelled, already_finished, same_customer, customer_not_found, customer_banned, already_a_guest · 429 slow down
appointments.complete
Mark as completed — Records that the appointment took place.
Needs appointments:write · changes data
https://app.getera.io/api/v1/appointments/{id}/actions/complete- Canonical address:
POST /api/v1/actions/appointments.complete
Input
| Field | Type | Required | Notes |
|---|---|---|---|
appointmentId | uuid | yes | Filled from the path {id} · Appointment · id of appointment |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
already_cancelled | A cancelled appointment cannot be completed. |
already_finished | It must not already be finished. |
not_yet_started | It must have started. |
Events it publishes: appointments.completed
What it writes: appointment (update) — Moves it to completed
curl -X POST https://app.getera.io/api/v1/appointments/APPOINTMENT_ID/actions/complete \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"appointments.completed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointments.complete("APPOINTMENT_ID")era appointments complete APPOINTMENT_ID{
"name": "era__appointments__complete",
"arguments": {
"appointmentId": "APPOINTMENT_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 already_cancelled, already_finished, not_yet_started · 429 slow down
appointments.confirm
Confirm an appointment — Accepts a pending booking.
Needs appointments:approve · changes data
https://app.getera.io/api/v1/appointments/{id}/actions/confirm- Canonical address:
POST /api/v1/actions/appointments.confirm
Input
| Field | Type | Required | Notes |
|---|---|---|---|
appointmentId | uuid | yes | Filled from the path {id} · Appointment · id of appointment |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_pending | Only a pending appointment can be confirmed. |
Events it publishes: appointments.confirmed
What it writes: appointment (update) — Moves it to confirmed
curl -X POST https://app.getera.io/api/v1/appointments/APPOINTMENT_ID/actions/confirm \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"appointments.confirmed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointments.confirm("APPOINTMENT_ID")era appointments confirm APPOINTMENT_ID{
"name": "era__appointments__confirm",
"arguments": {
"appointmentId": "APPOINTMENT_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:approve · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_pending · 429 slow down
appointments.export
Export appointments — Every booking in a period, one row per service line — the list turns them into a CSV or Excel file.
Needs appointments:export · reaches outside (sends, publishes or charges)
https://app.getera.io/api/v1/actions/appointments.exportInput
| Field | Type | Required | Notes |
|---|---|---|---|
from | date | yes | From |
to | date | yes | To |
status | string | Status · max 20 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
invalid_range | The period must not end before it starts. |
Events it publishes: appointments.exported
curl -X POST https://app.getera.io/api/v1/actions/appointments.export \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"from":"2026-11-02","to":"2026-11-02"}'{
"ok": true,
"result": "…",
"events": [
"appointments.exported"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.actions.run("appointments.export", {
from: "2026-11-02",
to: "2026-11-02"
})era run appointments.export \
--from "2026-11-02" \
--to "2026-11-02"{
"name": "era__appointments__export",
"arguments": {
"from": "2026-11-02",
"to": "2026-11-02"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:export · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 invalid_range · 429 slow down
appointments.import
Import appointments — Queues a mapped file, books every valid row and keeps a result for the summary screen.
Needs appointments:import · changes data
https://app.getera.io/api/v1/appointment_import_jobs/actions/import- Canonical address:
POST /api/v1/actions/appointments.import
Input
| Field | Type | Required | Notes |
|---|---|---|---|
fileName | string | yes | File · max 255 |
mapping | object / array | yes | Column mapping |
rows | object / array | yes | Prepared rows |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
invalid_import | The mapped file must contain no more than 2,000 usable rows. |
Events it publishes: appointments.import_completed
What it writes: appointment_import_job (insert) — Records and tracks the import; appointment_import_job (update) — Stores progress and the result; appointment (insert) — Books valid rows; appointment_guest (insert) — Adds each matched customer; appointment_guest_service (insert) — Adds the matched service and staff member
curl -X POST https://app.getera.io/api/v1/appointment_import_jobs/actions/import \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"fileName":"File","mapping":{},"rows":{}}'{
"ok": true,
"result": "…",
"events": [
"appointments.import_completed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointmentImportJobs.import({
fileName: "File",
mapping: {},
rows: {}
})era appointment_import_jobs import --data '{"fileName":"File","mapping":{},"rows":{}}'{
"name": "era__appointments__import",
"arguments": {
"fileName": "File",
"mapping": {},
"rows": {}
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:import · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 invalid_import · 429 slow down
appointments.lift_ban
Lift a ban — Lets that person book again.
Needs appointments:manage · removes or cannot be undone
https://app.getera.io/api/v1/banned_customers/{id}/actions/lift_ban- Canonical address:
POST /api/v1/actions/appointments.lift_ban
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Ban · id of banned_customer |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The ban must exist. |
Events it publishes: appointments.ban_lifted
What it writes: banned_customer (delete) — Removes the ban
curl -X POST https://app.getera.io/api/v1/banned_customers/BANNED_CUSTOMER_ID/actions/lift_ban \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"appointments.ban_lifted"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.bannedCustomers.liftBan("BANNED_CUSTOMER_ID")era banned_customers lift_ban BANNED_CUSTOMER_ID{
"name": "era__appointments__lift_ban",
"arguments": {
"id": "BANNED_CUSTOMER_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:manage · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found · 429 slow down
appointments.mark_deposit_status
Update booking deposit — Records the booking-side result of the shared finance payment flow.
Needs appointments:handle · changes data
https://app.getera.io/api/v1/appointments/{id}/actions/mark_deposit_status- Canonical address:
POST /api/v1/actions/appointments.mark_deposit_status
Input
| Field | Type | Required | Notes |
|---|---|---|---|
id | uuid | yes | Filled from the path {id} · Appointment · id of appointment |
status | string (enum) | yes | Deposit status · Allowed values: pending, paid, failed, refunded |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
appointment_not_found | The appointment must exist. |
deposit_not_required | The booking must require a deposit. |
Events it publishes: appointments.deposit_changed
What it writes: appointment (update) — Updates the frozen deposit status
curl -X POST https://app.getera.io/api/v1/appointments/APPOINTMENT_ID/actions/mark_deposit_status \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"status":"pending"}'{
"ok": true,
"result": "…",
"events": [
"appointments.deposit_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointments.markDepositStatus("APPOINTMENT_ID", {
status: "pending"
})era appointments mark_deposit_status APPOINTMENT_ID \
--status "pending"{
"name": "era__appointments__mark_deposit_status",
"arguments": {
"id": "APPOINTMENT_ID",
"status": "pending"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:handle · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 appointment_not_found, deposit_not_required · 429 slow down
appointments.no_show
Mark as no-show — Records that the customer did not arrive.
Needs appointments:write · changes data
https://app.getera.io/api/v1/appointments/{id}/actions/no_show- Canonical address:
POST /api/v1/actions/appointments.no_show
Input
| Field | Type | Required | Notes |
|---|---|---|---|
appointmentId | uuid | yes | Filled from the path {id} · Appointment · id of appointment |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
already_cancelled | A cancelled appointment cannot be a no-show. |
already_finished | It must not already be finished. |
not_yet_started | It must have started. |
Events it publishes: appointments.no_show
What it writes: appointment (update) — Moves it to no_show
curl -X POST https://app.getera.io/api/v1/appointments/APPOINTMENT_ID/actions/no_show \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"appointments.no_show"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointments.noShow("APPOINTMENT_ID")era appointments no_show APPOINTMENT_ID{
"name": "era__appointments__no_show",
"arguments": {
"appointmentId": "APPOINTMENT_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 already_cancelled, already_finished, not_yet_started · 429 slow down
appointments.raise_reminder
Raise a booking reminder — Marks a booking as due for its reminder and tells the notification rules. Run by the reminder job.
Needs appointments:handle · changes data
https://app.getera.io/api/v1/appointments/{id}/actions/raise_reminder- Canonical address:
POST /api/v1/actions/appointments.raise_reminder
Input
| Field | Type | Required | Notes |
|---|---|---|---|
appointmentId | uuid | yes | Filled from the path {id} · Appointment · id of appointment |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
reminders_off | Reminders must be switched on. |
not_found | The appointment must exist. |
not_upcoming | It must be pending or confirmed and still ahead. |
not_yet_due | It must be inside the reminder lead time. |
already_reminded | A booking is reminded once per slot. |
opted_out | At least one guest must accept reminders. |
no_workspace | The workspace must be readable, for its clock. |
Events it publishes: appointments.reminder_due
What it writes: appointment (update) — Stamps reminder_due_at
curl -X POST https://app.getera.io/api/v1/appointments/APPOINTMENT_ID/actions/raise_reminder \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"appointments.reminder_due"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointments.raiseReminder("APPOINTMENT_ID")era appointments raise_reminder APPOINTMENT_ID{
"name": "era__appointments__raise_reminder",
"arguments": {
"appointmentId": "APPOINTMENT_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:handle · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 reminders_off, not_found, not_upcoming, not_yet_due, already_reminded, opted_out, no_workspace · 429 slow down
appointments.remove_guest
Remove a guest — Takes one person off a booking along with everything they had booked.
Needs appointments:write · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/appointment_guests/{id}/actions/remove_guest- Canonical address:
POST /api/v1/actions/appointments.remove_guest
Input
| Field | Type | Required | Notes |
|---|---|---|---|
appointmentGuestId | uuid | yes | Filled from the path {id} · Guest · id of appointment_guest |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
guest_not_found | The guest must be on an appointment. |
already_cancelled | The appointment must not be cancelled. |
already_finished | The appointment must not be finished. |
last_guest | The last guest cannot be removed — cancel instead. |
Events it publishes: appointments.guest_removed
What it writes: appointment_guest (delete) — Removes the guest and their lines; appointment (update) — Re-totals duration, price and end time
curl -X POST https://app.getera.io/api/v1/appointment_guests/APPOINTMENT_GUEST_ID/actions/remove_guest \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"appointments.guest_removed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointmentGuests.removeGuest("APPOINTMENT_GUEST_ID")era appointment_guests remove_guest APPOINTMENT_GUEST_ID{
"name": "era__appointments__remove_guest",
"arguments": {
"appointmentGuestId": "APPOINTMENT_GUEST_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 guest_not_found, already_cancelled, already_finished, last_guest · 429 slow down
appointments.remove_service
Remove a service — Drops one service line and shortens the appointment to match.
Needs appointments:write · removes or cannot be undone · asks a person to confirm when Era runs it
https://app.getera.io/api/v1/appointment_guest_services/{id}/actions/remove_service- Canonical address:
POST /api/v1/actions/appointments.remove_service
Input
| Field | Type | Required | Notes |
|---|---|---|---|
lineId | uuid | yes | Filled from the path {id} · Service line · id of appointment_guest_service |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
line_not_found | The service line must exist. |
already_cancelled | The appointment must not be cancelled. |
already_finished | The appointment must not be finished. |
last_service | The last service cannot be removed — cancel instead. |
Events it publishes: appointments.service_removed
What it writes: appointment_guest_service (delete) — Removes the line; appointment_guest (delete) — Removes a guest left with nothing; appointment (update) — Re-totals duration, price and end time
curl -X POST https://app.getera.io/api/v1/appointment_guest_services/APPOINTMENT_GUEST_SERVICE_ID/actions/remove_service \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{}'{
"ok": true,
"result": "…",
"events": [
"appointments.service_removed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointmentGuestServices.removeService("APPOINTMENT_GUEST_SERVICE_ID")era appointment_guest_services remove_service APPOINTMENT_GUEST_SERVICE_ID{
"name": "era__appointments__remove_service",
"arguments": {
"lineId": "APPOINTMENT_GUEST_SERVICE_ID"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 line_not_found, already_cancelled, already_finished, last_service · 429 slow down
appointments.reschedule
Move the appointment — Moves a booking to another day or time, re-checking every rule against the new slot.
Needs appointments:write · changes data
https://app.getera.io/api/v1/appointments/{id}/actions/reschedule- Canonical address:
POST /api/v1/actions/appointments.reschedule
Input
| Field | Type | Required | Notes |
|---|---|---|---|
appointmentId | uuid | yes | Filled from the path {id} · Appointment · id of appointment |
appointmentDate | date | yes | New date |
startTime | string | yes | New start time · max 8 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The appointment must exist. |
already_cancelled | A cancelled appointment cannot be moved. |
already_finished | A finished appointment cannot be moved. |
unchanged | The new slot must differ from the current one. |
no_services | The appointment must still have service lines. |
inside_cutoff | The new time must respect the booking cutoff. |
outside_window | The new time must fall inside the booking window. |
service_not_offered_that_day | Every service must be offered on the new weekday. |
closed_that_day | The new date must not be closed by an override. |
outside_working_hours | The whole booking must fit inside opening hours. |
provider_on_leave | Nobody on the booking may be on leave then. |
slot_taken | Everyone on the booking must be free in the new window. |
class_full | A class must still have room at the new time. |
no_workspace | The workspace must be readable, for its clock. |
Events it publishes: appointments.rescheduled
What it writes: appointment (update) — Moves the date, start and end
curl -X POST https://app.getera.io/api/v1/appointments/APPOINTMENT_ID/actions/reschedule \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"appointmentDate":"2026-11-02","startTime":"New start time"}'{
"ok": true,
"result": "…",
"events": [
"appointments.rescheduled"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointments.reschedule("APPOINTMENT_ID", {
appointmentDate: "2026-11-02",
startTime: "New start time"
})era appointments reschedule APPOINTMENT_ID \
--appointment-date "2026-11-02" \
--start-time "New start time"{
"name": "era__appointments__reschedule",
"arguments": {
"appointmentId": "APPOINTMENT_ID",
"appointmentDate": "2026-11-02",
"startTime": "New start time"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found, already_cancelled, already_finished, unchanged, no_services, inside_cutoff, outside_window, service_not_offered_that_day, closed_that_day, outside_working_hours, provider_on_leave, slot_taken, class_full, no_workspace · 429 slow down
appointments.reschedule_by_token
Move by link — Whoever holds a booking’s manage link moves it to another free time under the same rules as the team; the old time is freed.
Needs appointments:write · changes data · Also callable from a published site (visitor)
/api/site/v1/{siteId}, e.g. with @getera/storefront). With an API key or a token it is refused (403 site_only).https://app.getera.io/api/v1/appointments/actions/reschedule_by_token- Canonical address:
POST /api/v1/actions/appointments.reschedule_by_token
Input
| Field | Type | Required | Notes |
|---|---|---|---|
token | string | yes | Link · max 100 |
appointmentDate | date | yes | New date |
startTime | string | yes | New start time · max 8 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
booking_link_invalid | The link must name a booking of this business. |
booking_started | Not once it has started. |
slot_taken | Everyone on the booking must be free at the new time. |
Events it publishes: appointments.rescheduled
What it writes: appointment (update) — Moves the date, start and end
Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 booking_link_invalid, booking_started, slot_taken · 429 slow down
appointments.reschedule_my_booking
Move my booking — A signed-in customer moves one of their own upcoming bookings to another free time, under the same booking rules as the team.
Needs appointments:write · changes data · Also callable from a published site (customer)
/api/site/v1/{siteId}, e.g. with @getera/storefront). With an API key or a token it is refused (403 site_only).https://app.getera.io/api/v1/appointments/{id}/actions/reschedule_my_booking- Canonical address:
POST /api/v1/actions/appointments.reschedule_my_booking
Input
| Field | Type | Required | Notes |
|---|---|---|---|
appointmentId | uuid | yes | Filled from the path {id} · Booking · id of appointment |
appointmentDate | date | yes | New date |
startTime | string | yes | New start time · max 8 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_your_booking | Only a booking the customer is a guest on. |
booking_started | Not once it has started. |
slot_taken | Everyone on the booking must be free in the new window. |
Events it publishes: appointments.rescheduled
What it writes: appointment (update) — Moves the date, start and end
Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_your_booking, booking_started, slot_taken · 429 slow down
appointments.set_reminder_opt_out
Booking reminders for a person — Records that a customer does, or does not, want booking reminders.
Needs appointments:write · removes or cannot be undone
https://app.getera.io/api/v1/appointment_reminder_opt_outs/actions/set_reminder_opt_out- Canonical address:
POST /api/v1/actions/appointments.set_reminder_opt_out
Input
| Field | Type | Required | Notes |
|---|---|---|---|
customerId | uuid | yes | Customer · id of customer |
optOut | boolean | yes | Do not remind |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
customer_not_found | The customer must exist. |
Events it publishes: appointments.reminder_opt_out_changed
What it writes: appointment_reminder_opt_out (insert) — Records the opt-out; appointment_reminder_opt_out (delete) — Removes it again
curl -X POST https://app.getera.io/api/v1/appointment_reminder_opt_outs/actions/set_reminder_opt_out \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"customerId":"CUSTOMER_ID","optOut":true}'{
"ok": true,
"result": "…",
"events": [
"appointments.reminder_opt_out_changed"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointmentReminderOptOuts.setReminderOptOut({
customerId: "CUSTOMER_ID",
optOut: true
})era appointment_reminder_opt_outs set_reminder_opt_out \
--customer-id "CUSTOMER_ID" \
--opt-out true{
"name": "era__appointments__set_reminder_opt_out",
"arguments": {
"customerId": "CUSTOMER_ID",
"optOut": true
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 customer_not_found · 429 slow down
appointments.update_details
Edit the note — The note staff read, and the address for a visit that is not at the business.
Needs appointments:write · changes data
https://app.getera.io/api/v1/appointments/{id}/actions/update_details- Canonical address:
POST /api/v1/actions/appointments.update_details
Input
| Field | Type | Required | Notes |
|---|---|---|---|
appointmentId | uuid | yes | Filled from the path {id} · Appointment · id of appointment |
notes | string | Note · max 2000 | |
address | string | Address · Home service only — where the visit happens. · max 500 |
Preconditions
The action is refused with 409 and one of these codes:
| Code | Refused when |
|---|---|
not_found | The appointment must exist. |
Events it publishes: appointments.details_updated
What it writes: appointment (update) — Stores the note and the address
curl -X POST https://app.getera.io/api/v1/appointments/APPOINTMENT_ID/actions/update_details \
-H "Authorization: Bearer $ERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"notes":"Note"}'{
"ok": true,
"result": "…",
"events": [
"appointments.details_updated"
]
}import { createEra } from "@getera/sdk"
const era = createEra({ apiKey: process.env.ERA_API_KEY })
const res = await era.appointments.updateDetails("APPOINTMENT_ID", {
notes: "Note"
})era appointments update_details APPOINTMENT_ID \
--notes "Note"{
"name": "era__appointments__update_details",
"arguments": {
"appointmentId": "APPOINTMENT_ID",
"notes": "Note"
}
}Errors: 401 no or expired credential · 403 the credential does not hold appointments:write · 404 the record or module is outside what it may reach · 422 invalid input (problems[]) · 409 not_found · 429 slow down
Data models
AppointmentGuestService
Table appointment_guest_service · Resource appointment_guest_services
GET /api/v1/appointment_guest_servicesGET /api/v1/appointment_guest_services/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
appointment_id | string (uuid) · uuid | appointment | ||
appointment_guest_id | string (uuid) · uuid | appointment_guest | ||
item_variant_id | string (uuid) · uuid | item_variant | ||
member_user_id | string (uuid) · uuid | yes | app_user | |
duration | integer · int4 | |||
price_amount | string (int64) · int8 | |||
price_currency | string · bpchar | yes | max 3 | |
sort_order | integer · int4 | |||
created_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK ((duration > 0))CHECK ((price_amount >= 0))
AppointmentGuest
Table appointment_guest · Resource appointment_guests
GET /api/v1/appointment_guestsGET /api/v1/appointment_guests/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
appointment_id | string (uuid) · uuid | appointment | ||
customer_id | string (uuid) · uuid | customer | ||
sort_order | integer · int4 | |||
created_at | string (date-time) · timestamptz |
Primary key: id
AppointmentImportJob
Table appointment_import_job · Resource appointment_import_jobs
GET /api/v1/appointment_import_jobsGET /api/v1/appointment_import_jobs/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
created_by | string (uuid) · uuid | app_user | ||
file_name | string · text | |||
mapping | object · jsonb | |||
status | string · text | queued, running, completed, failed | ||
total_rows | integer · int4 | |||
imported_rows | integer · int4 | |||
failed_rows | integer · int4 | |||
result | object · jsonb | |||
started_at | string (date-time) · timestamptz | yes | ||
finished_at | string (date-time) · timestamptz | yes | ||
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK ((failed_rows >= 0))CHECK ((imported_rows >= 0))CHECK ((total_rows >= 0))
AppointmentReminderOptOut
Table appointment_reminder_opt_out · Resource appointment_reminder_opt_outs
GET /api/v1/appointment_reminder_opt_outsGET /api/v1/appointment_reminder_opt_outs/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
customer_id | string (uuid) · uuid | customer | ||
created_by | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz |
Primary key: id
Unique: workspace_id, customer_id
Appointment
Table appointment · Resource appointments
GET /api/v1/appointmentsGET /api/v1/appointments/{id}POST /api/v1/appointments→appointments.book
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
appointment_date | string (date) · date | |||
start_time | string (time) · time | |||
end_time | string (time) · time | |||
total_duration | integer · int4 | |||
total_price_amount | string (int64) · int8 | |||
total_price_currency | string · bpchar | yes | max 3 | |
status | string · text | pending, confirmed, completed, cancelled, no_show | ||
notes | string · text | yes | ||
cancellation_reason | string · text | yes | ||
cancelled_at | string (date-time) · timestamptz | yes | ||
owner_user_id | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz | |||
updated_at | string (date-time) · timestamptz | |||
address | string · text | yes | ||
reminder_due_at | string (date-time) · timestamptz | yes | ||
deposit_amount | string (int64) · int8 | |||
deposit_status | string · text | not_required, required, pending, paid, failed, refunded | ||
booked_online_at | string (date-time) · timestamptz | yes |
Primary key: id
Checks:
CHECK ((end_time > start_time))CHECK (((status = 'cancelled'::text) = (cancelled_at IS NOT NULL)))CHECK ((deposit_amount >= 0))CHECK ((total_duration >= 0))CHECK ((total_price_amount >= 0))
BannedCustomer
Table banned_customer · Resource banned_customers
GET /api/v1/banned_customersGET /api/v1/banned_customers/{id}
| Column | Type | Nullable | Allowed values | References |
|---|---|---|---|---|
id | string (uuid) · uuid | |||
organization_id | string (uuid) · uuid | organization | ||
workspace_id | string (uuid) · uuid | workspace | ||
email | string · text | yes | ||
phone | string · text | yes | ||
reason | string · text | yes | ||
notes | string · text | yes | ||
banned_by | string (uuid) · uuid | yes | app_user | |
created_at | string (date-time) · timestamptz |
Primary key: id
Checks:
CHECK (((email IS NOT NULL) OR (phone IS NOT NULL)))
Events
Workflows and webhooks react to these events.
| Event | What happens | Payload |
|---|---|---|
appointments.ban_lifted | A ban was removed. | id |
appointments.booked | A slot was reserved. | id, appointmentDate, startTime, customerId, itemVariantId, memberUserId, totalAmount, currency, depositAmount |
appointments.cancelled | A booking was cancelled and the slot freed. | id, appointmentDate, startTime, reason, customerId |
appointments.completed | The appointment took place. | id, customerId, customerName, appointmentDate, startTime, memberUserId, itemVariantId |
appointments.confirmed | A pending booking was accepted. | id, appointmentDate, customerId, totalAmount, currency |
appointments.customer_banned | Someone was stopped from booking. | id, email, phone |
appointments.deposit_changed | A required booking deposit changed state through finance. | id, from, to, amount |
appointments.details_updated | The note or the address changed. | id |
appointments.exported | Bookings were taken out as a file. | count, from, to, truncated |
appointments.guest_added | Another person joined an appointment. | id, guestId, customerId, lineId |
appointments.guest_customer_changed | A guest’s place was given to another person. | id, guestId, fromCustomerId, customerId, lineIds |
appointments.guest_removed | Someone was taken off a booking. | id, guestId |
appointments.import_completed | A mapped appointment file finished importing. | id, imported, failed |
appointments.member_assigned | A service line changed hands. | id, lineId, memberUserId |
appointments.no_show | The customer did not arrive. | id, customerId, customerName, appointmentDate, startTime, memberUserId, itemVariantId |
appointments.reminder_due | A booking is inside its reminder lead time — notification rules send it. | id, appointmentDate, startTime, customerId, customerIds, channels, hoursBefore, timezone |
appointments.reminder_opt_out_changed | A customer started or stopped accepting booking reminders. | customerId, optOut |
appointments.rescheduled | A booking moved to another slot. | id, fromDate, fromStartTime, appointmentDate, startTime, customerId |
appointments.service_added | Another service was added to an appointment. | id, lineId, itemVariantId |
appointments.service_removed | A service was dropped from a booking. | id, lineId |