Articles on: General

Apprentice API Reference

Overview


Apprentice is a booking and client management platform for tattoo, piercing and body art artists and shops. This page documents the API behind the Apprentice integration for Zapier. You do not need it to build Zaps. Use it to check what a step sends and receives, to debug an error, or to call the API yourself.


Base URL


https://app.useapprentice.com/api/integrations/zapier


Version. The API version is a date. This page describes 2026-02-01. Each connection is pinned to the version it was created on.


Data format. Requests and responses are JSON. Times are ISO 8601 in UTC unless a field says otherwise.


What a connection can see. A connection is tied to one workspace. An artist connection can reach that artist's data. A shop connection can reach data for the shop's artists. Nothing else is returned.


Sensitive data. The API does not return waiver text, signatures, medical or allergy answers, guardian consent state, internal notes or pricing. Details are in What the API does not return.


Endpoint index


Every path below is relative to the base URL. Send Authorization: Bearer <access_token> on every call. Send Content-Type: application/json when you include a JSON body. {id} means the UUID of the appointment, client or subscription named by the path.


These are all 19 operations called by the current Zapier integration, plus the two hook maintenance operations described here. OAuth authorization and token endpoints are documented in Authentication. Payment actions were removed in version 2.0.0 and are not part of this integration.


Method

Path

Required scope

Request and response

GET

/auth/test

Valid token

Connection test

POST

/hooks

hooks:write

Subscribe

DELETE

/hooks/{id}

hooks:write

Unsubscribe

GET

/hooks

hooks:write

List subscriptions

PATCH

/hooks/{id}

hooks:write

Re-enable a subscription

POST

/appointments

appointments:write

Create Appointment

PATCH

/appointments/{id}

appointments:write

Update Appointment

POST

/appointments/{id}/confirm

appointments:write

Confirm Appointment

DELETE

/appointments/{id}

appointments:write

Cancel Appointment

POST

/appointments/{id}/notes

notes:write

Add Appointment Note

GET

/appointments/search

appointments:read

Find Appointment

POST

/clients

clients:write

Create Client

POST

/clients/find-or-create

clients:write

Find or Create Client

PATCH

/clients/{id}

clients:write

Update Client

POST

/clients/{id}/notes

notes:write

Add Client Note

GET

/clients/search

clients:read

Find Client

POST

/notifications

notifications:write

Send Notification

POST

/booking-requests

booking_submissions:write

Create Booking Request

POST

/booking-submissions

booking_submissions:write

Book Appointment From Booking Page

GET

/locations

locations:read

Locations

GET

/booking-pages

booking_pages:read

Booking pages


Contents


  1. Authentication
  2. Shared behavior
  3. Events (triggers)
  4. Actions
  5. Searches and dropdowns
  6. Services with extra rules
  7. Errors
  8. What the API does not return
  9. Changelog



Authentication


Apprentice uses OAuth 2.0 with the authorization code flow and PKCE. You do not enter an API key. Sign in once and approve access.


Item

Value

Authorize URL

https://qgrdfaeclgcyrgmafatm.supabase.co/auth/v1/oauth/authorize

Token URL

https://qgrdfaeclgcyrgmafatm.supabase.co/auth/v1/oauth/token

Refresh URL

Same as the token URL

Grant types

authorization_code, refresh_token

PKCE

Yes. Method S256.

Client authentication

HTTP Basic header with the client ID and secret. Not in the body.

Token type

Bearer


The Apprentice sign-in service is hosted on Supabase, which is why the URLs are on a supabase.co address. The API itself is on app.useapprentice.com.


The flow


  1. Send the user to the authorize URL with client_id, response_type=code, redirect_uri, state, code_challenge and code_challenge_method=S256.
  2. The user signs in to Apprentice. A consent screen asks which workspace to connect: their own artist account, or a shop where they are an owner or admin. The user approves.
  3. Apprentice redirects to your redirect_uri with a code and your state.
  4. Exchange the code with a POST to the token URL. The body is application/x-www-form-urlencoded: grant_type=authorization_code, code, redirect_uri, code_verifier.
  5. Send Authorization: Bearer <access_token> on every API call.


Refresh


When the access token expires, or after any 401, POST to the token URL with grant_type=refresh_token and refresh_token. The response can include a new refresh token. Always store the newest one. The token response includes expires_in (seconds). Refresh before it runs out.


OAuth request examples


Zapier supplies the registered redirect URI, state and PKCE values. Other OAuth clients need their own approved client credentials and registered redirect URI. An ordinary Apprentice session token cannot replace this connection.


Authorization request (open in the user's browser):


GET https://qgrdfaeclgcyrgmafatm.supabase.co/auth/v1/oauth/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_URL_ENCODED_REDIRECT_URI&state=YOUR_RANDOM_STATE&code_challenge=YOUR_S256_CHALLENGE&code_challenge_method=S256


Check that the returned state matches the one you sent. Exchange the authorization code:


curl --request POST \
  'https://qgrdfaeclgcyrgmafatm.supabase.co/auth/v1/oauth/token' \
  --user "$CLIENT_ID:$CLIENT_SECRET" \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode "code=$AUTHORIZATION_CODE" \
  --data-urlencode "redirect_uri=$REDIRECT_URI" \
  --data-urlencode "code_verifier=$CODE_VERIFIER"


Successful token responses contain access_token, refresh_token, token_type and expires_in. token_type is bearer; expires_in is the lifetime in seconds. Use the returned lifetime rather than hardcoding one.


Refresh request:


curl --request POST \
  'https://qgrdfaeclgcyrgmafatm.supabase.co/auth/v1/oauth/token' \
  --user "$CLIENT_ID:$CLIENT_SECRET" \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=refresh_token' \
  --data-urlencode "refresh_token=$REFRESH_TOKEN"


The OAuth service has its own error responses. The API error envelope later on this page applies to the Apprentice API base URL. If refresh fails or the connection was disconnected, reconnect through Zapier.


Scopes


The authorize request does not choose scopes. When the user approves, the connection receives all of the scopes below. They are fixed for the life of the connection.


Scope

What it allows

appointments:read

Find appointments

appointments:write

Create, update, confirm and cancel appointments

clients:read

Find clients

clients:write

Create, find or create, and update clients

notes:write

Add notes to appointments and clients

notifications:write

Send notifications to clients

booking_submissions:write

Create booking requests and book from a booking page

booking_pages:read

List booking pages

locations:read

List locations

hooks:write

Subscribe to and unsubscribe from events

consent:read, payments:read, payments:write

Granted with the connection. The Zapier integration does not use them. It has no waiver-reading or payment steps.


A call without the scope it needs returns 403 OWNER_SCOPE_VIOLATION.


Connection test


GET /auth/test. Needs a valid token and no scope. Zapier calls it when a user connects.


{
  "data": {
    "owner_type": "artist",
    "owner_id": "3f1c2a9e-5b7d-4e0a-9c11-7d2f6a8b4e10",
    "environment": "live",
    "scopes": ["appointments:read", "appointments:write", "clients:read"],
    "pinned_api_version": "2026-02-01",
    "label": "Alex Smith",
    "auth_type": "oauth",
    "connection_id": "9d8e7c6b-5a4f-4321-8a0b-1c2d3e4f5a6b"
  }
}


label is the account name for an artist, the shop name for a shop, or the account email if no name is set. Zapier shows it as Alex Smith (artist) in the connection list. It never contains a token or secret.


Disconnecting


A user can disconnect in Apprentice settings. The connection stops working, and its event subscriptions are turned off. A shop connection also stops working if the person who connected it is no longer an owner or admin of the shop. To use Apprentice again, connect again.



Shared behavior


Make an API request


All IDs, dates and names in examples are illustrative. Replace them with records in your connected workspace. Running a write example creates or changes real data. No example requires a payment or a charge.


export APPRENTICE_API='https://app.useapprentice.com/api/integrations/zapier'
# Set ACCESS_TOKEN privately to the access_token returned by OAuth.
curl "$APPRENTICE_API/auth/test" \
  --header "Authorization: Bearer $ACCESS_TOKEN"


For a write, send a distinct idempotency key and the JSON body shown under that endpoint:


curl --request POST "$APPRENTICE_API/clients" \
  --header "Authorization: Bearer $ACCESS_TOKEN" \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: external-form-submission-12345' \
  --data '{"email":"jane@example.com","first_name":"Jane","last_name":"Doe"}'


This example uses an artist connection. A shop connection also needs artist_id. Keep the key when retrying this same operation; change it for a different operation.


Idempotency


Send an Idempotency-Key header on writes. Use one new value for each logical operation.


  • The same key and the same body within 24 hours returns the first response, with the header X-Idempotency-Replay: true. The write is not repeated.
  • The same key with a different body returns 409 with a plain body: { "error": "Idempotency key conflict: same key used with different request body" }. This is the one error that does not use the error shape below.
  • Server errors (5xx) and short-lived conflicts are not stored, so a retry runs again.
  • Keys are scoped to the connection.
  • If a request with the same key is still running, you get 409 IDEMPOTENCY_REQUEST_IN_PROGRESS with Retry-After: 1. Retry.


The Zapier integration builds the key from the step name and its inputs, so a Zapier retry never creates a duplicate. Subscribing to an event does not use this cache. It is safe to repeat by design.


Pagination


List endpoints use cursors.


Parameter

Meaning

limit

Page size, 1 to 100. Default 20.

cursor

The pagination.nextCursor from the last page. Leave out for page 1.


{
  "data": [ ],
  "pagination": { "nextCursor": "eyJ2IjoiMjAyNi0wMS0xMiIsImlkIjoiLi4uIn0", "hasMore": true, "limit": 20, "returned": 20 }
}


nextCursor is null on the last page. Treat cursors as opaque. A malformed cursor returns 400 VALIDATION_INVALID_FORMAT.


Rate limits


Each connection can make 30 requests per minute and 5,000 per day (UTC day). Every response to an authenticated request carries these headers. A request that fails before the limit is checked (a missing or bad token, or a missing scope) does not, though it still has X-Request-ID.


Header

Meaning

X-RateLimit-Limit

The limit for the window

X-RateLimit-Remaining

Requests left in the window

X-RateLimit-Reset

When the window resets, in Unix seconds

X-Request-ID

A request ID. Quote it to support.


When a limit is hit you get 429 RATE_LIMIT_EXCEEDED and a Retry-After header (seconds). The headers then describe whichever window ran out. Repeated bad tokens from one network address are also limited.


A connection can hold up to 10 event subscriptions. A subscribe past that returns 400 VALIDATION_OUT_OF_RANGE.


Errors


Except for the idempotency body-conflict response described above, API errors have this shape:


{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "start_time: start_time must be a valid ISO 8601 datetime",
    "requestId": "3b1f6a52-8c0d-4e79-a1b4-5d2c7e9f0a18",
    "timestamp": "2026-03-01T14:00:00.000Z",
    "details": []
  }
}


Branch on code. The message is written for a person. details appears only on 4xx errors. See Errors for the codes.



Events (triggers)


Triggers are instant. Apprentice sends an event the moment something happens. There is no polling.


Subscribe


POST /hooks. Scope hooks:write. Zapier calls it when a Zap is turned on.


{ "event": "appointment.created", "target_url": "https://hooks.zapier.com/hooks/standard/123456/abcdef/" }


Response 201:


{
  "id": "0b2f6c1e-7d3a-4c58-9e41-2a6d8f0b1c33",
  "event": "appointment.created",
  "target_url": "https://hooks.zapier.com/hooks/standard/123456/abcdef/",
  "signing_secret": "5f2b1c0e9a8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a291807f6e5d4c3b2a19087"
}


  • target_url must use HTTPS. Localhost and private network addresses are refused (400 VALIDATION_INVALID_URL).
  • Subscribing again with the same event and URL returns the existing subscription, and turns it back on if it was disabled.
  • An unknown event returns 400 VALIDATION_INVALID_ENUM.


Unsubscribe


DELETE /hooks/{id}. Scope hooks:write. Zapier calls it when a Zap is turned off or deleted. Response 200: { "success": true }. Pending deliveries are removed with the subscription. Errors: 400 VALIDATION_INVALID_UUID, 404 RESOURCE_NOT_FOUND (no subscription with this ID on this connection, or it was already deleted).


List subscriptions


GET /hooks. Scope hooks:write. No body or query parameters. This maintenance call is not used by the Zapier app. It returns the connection's subscriptions, newest first, without signing secrets. It does not use cursor pagination.


Response 200:


{
  "data": [
    {
      "id": "0b2f6c1e-7d3a-4c58-9e41-2a6d8f0b1c33",
      "event": "appointment.created",
      "targetUrl": "https://hooks.zapier.com/hooks/standard/123456/abcdef/",
      "status": "active",
      "environment": "live",
      "consecutiveFailures": 0,
      "disabledAt": null,
      "disabledReason": null,
      "createdAt": "2026-03-01T12:00:00.000Z"
    }
  ]
}


An empty list returns { "data": [] }. Shared authentication, permission, rate-limit and server errors apply.


Re-enable a subscription


PATCH /hooks/{id}. Scope hooks:write. This maintenance call is not used by the Zapier app. Use the subscription ID returned by Subscribe or List subscriptions.


Request:


{ "status": "active" }


Response 200:


{ "success": true }


This resets the failure count and clears the disabled reason. It does not replay exhausted deliveries. An invalid UUID or any status other than active returns 400. Shared errors also apply.


Event list


Zapier trigger

Event

resource_type

New Appointment

appointment.created

appointment

Appointment Status Changed

appointment.status_updated

appointment

New Client Profile

client.created

client

New Scheduled Booking

booking.submitted

booking_submission

New Booking Request

booking.requested

booking_request

Waiver Signed

consent.signed

consent

Client Checked In

checkin.completed

visit


Envelope


Every event has the same outer shape. The event details are in data.


Field

Meaning

id

Event ID (UUID)

event

Event name, for example appointment.created

api_version

2026-02-01

created_at

When the event was created

resource_type

See the table above

resource_id

ID of the thing the event is about

data

The payload, below


Webhook delivery


Apprentice sends each event as an HTTPS POST with Content-Type: application/json and these headers:


Header

Value

X-Apprentice-Event

The event name

X-Apprentice-Delivery

A unique delivery ID. Use it to ignore repeats.

X-Apprentice-Api-Version

2026-02-01

X-Apprentice-Signature

t=<unix seconds>,v1=<hex signature>

User-Agent

Apprentice-Webhooks/1.0


Signature. v1 is a hex HMAC-SHA256. The key is the signing_secret from the subscribe response. The signed text is the timestamp, a period, then the raw request body: <t>.<body>. Compute it, compare in constant time, and reject an old t. Zapier does not check signatures, so Zaps need nothing extra.


const crypto = require("crypto");
const [t, v1] = header.split(",").map((part) => part.split("=")[1]);
const expected = crypto.createHmac("sha256", signingSecret).update(`${t}.${rawBody}`).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));


Success and retries.


  • Any 2xx response counts as delivered. Reply within 30 seconds.
  • Any other status, a timeout, or a redirect counts as a failure. Redirects are not followed.
  • A failed delivery is tried again, up to 5 attempts in total. The waits are about 1 minute, 5 minutes, 30 minutes and 2 hours, each with a little random jitter.
  • After the last attempt the delivery is dropped.
  • After 10 failed attempts in a row, the subscription is disabled. Any success resets the count. Re-enable with PATCH /hooks/{id}, or subscribe again.


Events can arrive more than once, and not always in order. Use X-Apprentice-Delivery or the event id to ignore repeats.


New Appointment: appointment.created


data fields:


Field

Type

Notes

id

string

Appointment ID

artist_id, client_id

string


start_time, end_time

string


status

string

For example scheduled

appointment_type

string

The kind of session: consultation, tattoo, touch_up or healing_check. For the service, use service_type_slug.

title

string or null


service_type_slug

string

tattoo, piercing, pmu, laser_removal, scarification, tooth_gem or implants

service_display_name

string

For example Tattoo

service_summary_line

string

Short label for lists

service_payload

object

Service details. {} if none. Health keys are removed.

location_id, client_email, client_first_name, client_last_name, client_phone, artist_first_name, artist_last_name, location_name, location_timezone

string

Optional. Left out when not known.


{
  "id": "8f3c1a7e-2b9d-4c60-a5e1-3d7f0b9c2a44",
  "event": "appointment.created",
  "api_version": "2026-02-01",
  "created_at": "2026-03-01T14:00:00.000Z",
  "resource_type": "appointment",
  "resource_id": "2c8e5b1f-0a47-4d93-b6e2-9f3a7c5d1e80",
  "data": {
    "id": "2c8e5b1f-0a47-4d93-b6e2-9f3a7c5d1e80",
    "artist_id": "3f1c2a9e-5b7d-4e0a-9c11-7d2f6a8b4e10",
    "client_id": "6a1d4c52-93be-4f07-8b2a-0c7e5d9f1a48",
    "location_id": "5d9b2e71-4a06-4c83-b1f7-0e8a3c6d2f95",
    "start_time": "2026-03-01T14:00:00.000Z",
    "end_time": "2026-03-01T16:00:00.000Z",
    "status": "scheduled",
    "appointment_type": "consultation",
    "title": "New Tattoo Session",
    "client_email": "jane@example.com",
    "client_first_name": "Jane",
    "client_last_name": "Doe",
    "client_phone": "+15551234567",
    "artist_first_name": "Alex",
    "artist_last_name": "Smith",
    "location_name": "Downtown Studio",
    "location_timezone": "America/New_York",
    "service_type_slug": "tattoo",
    "service_display_name": "Tattoo",
    "service_summary_line": "Tattoo",
    "service_payload": {}
  }
}


Appointment Status Changed: appointment.status_updated


Sent when an appointment is confirmed, rescheduled, cancelled, completed or marked no-show. data has every field of appointment.created, plus previous_status (string).


{
  "id": "1e6a9d30-7c52-4b88-8f14-6a0c3e5b7d21",
  "event": "appointment.status_updated",
  "api_version": "2026-02-01",
  "created_at": "2026-03-01T14:00:00.000Z",
  "resource_type": "appointment",
  "resource_id": "2c8e5b1f-0a47-4d93-b6e2-9f3a7c5d1e80",
  "data": {
    "id": "2c8e5b1f-0a47-4d93-b6e2-9f3a7c5d1e80",
    "artist_id": "3f1c2a9e-5b7d-4e0a-9c11-7d2f6a8b4e10",
    "client_id": "6a1d4c52-93be-4f07-8b2a-0c7e5d9f1a48",
    "start_time": "2026-03-01T14:00:00.000Z",
    "end_time": "2026-03-01T16:00:00.000Z",
    "status": "confirmed",
    "previous_status": "scheduled",
    "appointment_type": "tattoo",
    "title": "Tattoo Session",
    "service_type_slug": "tattoo",
    "service_display_name": "Tattoo",
    "service_summary_line": "Tattoo",
    "service_payload": {}
  }
}


New Client Profile: client.created


data: id, artist_id, first_name, last_name, email (string or null), phone (string or null). At least one of email and phone is set.


{
  "id": "4a7c2e90-1b35-4d68-9f03-8e5a1c7b3d52",
  "event": "client.created",
  "api_version": "2026-02-01",
  "created_at": "2026-03-01T12:00:00.000Z",
  "resource_type": "client",
  "resource_id": "6a1d4c52-93be-4f07-8b2a-0c7e5d9f1a48",
  "data": {
    "id": "6a1d4c52-93be-4f07-8b2a-0c7e5d9f1a48",
    "artist_id": "3f1c2a9e-5b7d-4e0a-9c11-7d2f6a8b4e10",
    "first_name": "Jane",
    "last_name": "Doe",
    "email": "jane@example.com",
    "phone": "+15551234567"
  }
}


New Scheduled Booking: booking.submitted


Sent when a client books an exact appointment from a booking page.


Field

Type

Notes

id

string

Booking submission ID

appointment_id, artist_id, client_id

string


guest_name, guest_email

string


submitted_at, start_time, end_time

string


service_type_slug, service_display_name, service_summary_line, service_payload


Same as appointments

guest_phone, booking_page_id, location_id, artist_name, location_name

string

Optional


{
  "id": "9c2e4a60-3d17-4f85-b0a9-7e1d5c3b8f26",
  "event": "booking.submitted",
  "api_version": "2026-02-01",
  "created_at": "2026-03-01T14:00:00.000Z",
  "resource_type": "booking_submission",
  "resource_id": "5b8d0f12-6a34-4c97-8e21-1f9a3c7d5e40",
  "data": {
    "id": "5b8d0f12-6a34-4c97-8e21-1f9a3c7d5e40",
    "appointment_id": "2c8e5b1f-0a47-4d93-b6e2-9f3a7c5d1e80",
    "artist_id": "3f1c2a9e-5b7d-4e0a-9c11-7d2f6a8b4e10",
    "client_id": "6a1d4c52-93be-4f07-8b2a-0c7e5d9f1a48",
    "guest_name": "Jane Doe",
    "guest_email": "jane@example.com",
    "submitted_at": "2026-03-01T14:00:00.000Z",
    "start_time": "2026-03-01T14:00:00.000Z",
    "end_time": "2026-03-01T16:00:00.000Z",
    "service_type_slug": "tattoo",
    "service_display_name": "Tattoo",
    "service_summary_line": "Tattoo",
    "service_payload": {},
    "guest_phone": "+15551234567",
    "booking_page_id": "4e7a9c21-6d38-4b15-a0f2-8c1b3d5e7f90",
    "location_id": "5d9b2e71-4a06-4c83-b1f7-0e8a3c6d2f95",
    "artist_name": "Alex Smith",
    "location_name": "Downtown Studio"
  }
}


New Booking Request: booking.requested


Sent when a pending request is created, from a booking page or from the Create Booking Request action.


Field

Type

Notes

id

string

Request ID

artist_id, booking_page_id

string


status

string

Always pending

client_name, client_email

string


requested_at

string


verification_required

boolean

Always true

service_type_slug, service_display_name, service_summary_line, service_payload


Same as appointments

client_phone, preferred_dates_text

string

Optional


{
  "id": "6d0f2b84-9a31-4e57-a8c3-2b7e1d5f9a60",
  "event": "booking.requested",
  "api_version": "2026-02-01",
  "created_at": "2026-03-01T14:00:00.000Z",
  "resource_type": "booking_request",
  "resource_id": "7b3d1f95-2e48-4a60-91c7-5d0e8a2b4c16",
  "data": {
    "id": "7b3d1f95-2e48-4a60-91c7-5d0e8a2b4c16",
    "artist_id": "3f1c2a9e-5b7d-4e0a-9c11-7d2f6a8b4e10",
    "booking_page_id": "4e7a9c21-6d38-4b15-a0f2-8c1b3d5e7f90",
    "status": "pending",
    "client_name": "Jane Doe",
    "client_email": "jane@example.com",
    "requested_at": "2026-03-01T14:00:00.000Z",
    "verification_required": true,
    "service_type_slug": "tattoo",
    "service_display_name": "Tattoo",
    "service_summary_line": "Tattoo",
    "service_payload": {},
    "client_phone": "+15551234567",
    "preferred_dates_text": "Weekday afternoons in September"
  }
}


Waiver Signed: consent.signed


Sent when a client signs a waiver. The event says that a waiver was signed, and when. It does not include the waiver text, the client's answers or the signature.


data: prep_request_id, template_id, signed_at. No other fields.


{
  "id": "3a5c7e19-4b20-4d86-9f71-0c2e8a6b1d35",
  "event": "consent.signed",
  "api_version": "2026-02-01",
  "created_at": "2026-03-01T13:45:00.000Z",
  "resource_type": "consent",
  "resource_id": "8d1f3b57-2c64-4a90-b5e8-6a0c9e2d4f71",
  "data": {
    "prep_request_id": "8d1f3b57-2c64-4a90-b5e8-6a0c9e2d4f71",
    "template_id": "0f4a6c82-1e53-4b79-8d20-5c7b9e3a1d64",
    "signed_at": "2026-03-01T13:45:00.000Z"
  }
}


Client Checked In: checkin.completed


data: visit_id, action (check_in or confirm_arrival), checked_in_at, and optional artist_id and shop_id.


{
  "id": "2d4f6a81-5c39-4e07-a1b6-8e3c0d7f9a52",
  "event": "checkin.completed",
  "api_version": "2026-02-01",
  "created_at": "2026-03-01T13:55:00.000Z",
  "resource_type": "visit",
  "resource_id": "9e2a4c60-7b18-4d35-8f91-3a5d1c7e0b84",
  "data": {
    "visit_id": "9e2a4c60-7b18-4d35-8f91-3a5d1c7e0b84",
    "action": "check_in",
    "checked_in_at": "2026-03-01T13:55:00.000Z",
    "artist_id": "3f1c2a9e-5b7d-4e0a-9c11-7d2f6a8b4e10",
    "shop_id": "1a3c5e70-9b24-4f68-8d02-7c4e6a8b0d93"
  }
}



Actions


All action endpoints need the scope shown. All writes accept Idempotency-Key. Responses wrap the result in { "data": ... }.


Zapier action

Method and path

Scope

Create Appointment

POST /appointments

appointments:write

Update Appointment

PATCH /appointments/{id}

appointments:write

Confirm Appointment

POST /appointments/{id}/confirm

appointments:write

Cancel Appointment

DELETE /appointments/{id}

appointments:write

Add Appointment Note

POST /appointments/{id}/notes

notes:write

Create Client

POST /clients

clients:write

Find or Create Client

POST /clients/find-or-create

clients:write

Update Client

PATCH /clients/{id}

clients:write

Add Client Note

POST /clients/{id}/notes

notes:write

Send Notification

POST /notifications

notifications:write

Create Booking Request

POST /booking-requests

booking_submissions:write

Book Appointment From Booking Page

POST /booking-submissions

booking_submissions:write


The appointment object


Create, update, confirm and cancel return an appointment. So does Find Appointment. These are the only fields it has:


Field

Type

Notes

id, artist_id, client_id, location_id, project_id

string (UUID) or null


start_time, end_time

string


status

string

pending_verification, scheduled, confirmed, in_progress, completed, cancelled, rescheduled or no_show

appointment_type

string

consultation, tattoo, touch_up or healing_check

title

string or null


confirmed_at, cancelled_at, created_at, updated_at

string or null


client_email, client_first_name, client_last_name, client_phone

string or null


artist_first_name, artist_last_name

string or null


location_name, location_timezone

string or null


service_type_slug, service_display_name, service_summary_line, service_payload


As in the events


{
  "data": {
    "id": "2c8e5b1f-0a47-4d93-b6e2-9f3a7c5d1e80",
    "artist_id": "3f1c2a9e-5b7d-4e0a-9c11-7d2f6a8b4e10",
    "client_id": "6a1d4c52-93be-4f07-8b2a-0c7e5d9f1a48",
    "location_id": "5d9b2e71-4a06-4c83-b1f7-0e8a3c6d2f95",
    "project_id": "0c6e8a24-3f51-4b97-9d10-2a4c6e8f1b73",
    "start_time": "2026-03-01T14:00:00.000Z",
    "end_time": "2026-03-01T16:00:00.000Z",
    "status": "confirmed",
    "appointment_type": "tattoo",
    "title": "Forearm session",
    "confirmed_at": "2026-02-20T09:30:00.000Z",
    "cancelled_at": null,
    "created_at": "2026-02-18T17:05:00.000Z",
    "updated_at": "2026-02-20T09:30:00.000Z",
    "client_email": "jane@example.com",
    "client_first_name": "Jane",
    "client_last_name": "Doe",
    "client_phone": "+15551234567",
    "artist_first_name": "Alex",
    "artist_last_name": "Smith",
    "location_name": "Downtown Studio",
    "location_timezone": "America/New_York",
    "service_type_slug": "tattoo",
    "service_display_name": "Tattoo",
    "service_summary_line": "Tattoo",
    "service_payload": {}
  }
}


Create Appointment


POST /appointments


Body field

Required

Notes

client_id

Yes

UUID of one of your clients

start_time, end_time

Yes

ISO 8601 date and time

artist_id

Shops: yes

UUID. Leave out on an artist connection to book on your own calendar. An artist connection can only name itself.

location_id

No

Defaults to the artist's primary location

project_id

No

An existing project for this client and artist. A new project is created if you leave it out.

title

No


appointment_type

No

consultation, tattoo, touch_up, healing_check. Default tattoo.

service_type_slug

No

See service values. Default tattoo, or the linked project's service.

notes

No


status

No

Default scheduled

scarificationTechnique, scarificationHealingIntent

Scarification

See Scarification


Example request body:


{
  "client_id": "6a1d4c52-93be-4f07-8b2a-0c7e5d9f1a48",
  "start_time": "2026-11-10T15:00:00.000Z",
  "end_time": "2026-11-10T16:00:00.000Z"
}


Response 201: an appointment. Apprentice checks for a time conflict first.


Errors: 400 VALIDATION_FAILED, 400 VALIDATION_REQUIRED_FIELD (shop key without artist_id), 403, 404 RESOURCE_NOT_FOUND (client not yours), 409 APPOINTMENT_CONFLICT.


Update Appointment


PATCH /appointments/{id}. Send only what changes: start_time, end_time, location_id, title, notes, status, appointment_type.


  • Changing the time re-checks for conflicts (409 APPOINTMENT_CONFLICT).
  • A status change has to be a valid move. Otherwise: 400 VALIDATION_FAILED, "Invalid status transition from 'cancelled' to 'confirmed'".
  • Completing a minor's appointment without the required guardian verification returns 409 GUARDIAN_VERIFICATION_REQUIRED.


Example request body:


{
  "title": "Consultation follow-up"
}


Response 200: an appointment. Errors also include 404 APPOINTMENT_NOT_FOUND.


Confirm Appointment


POST /appointments/{id}/confirm. No body. Moves the appointment to confirmed.


Example request (with your Bearer header):


POST /appointments/2c8e5b1f-0a47-4d93-b6e2-9f3a7c5d1e80/confirm


Response 200: an appointment. Errors: 404 APPOINTMENT_NOT_FOUND, 422 APPOINTMENT_ALREADY_CONFIRMED, 400 VALIDATION_FAILED (status cannot move to confirmed), 422 BUSINESS_LOGIC_VIOLATION ("Deposit payment is required before confirming this appointment", when the booking page needs a deposit that has not been paid).


Cancel Appointment


DELETE /appointments/{id}. No body. Sets the status to cancelled. The appointment is kept. A linked visit is cancelled too.


Example request (with your Bearer header):


DELETE /appointments/2c8e5b1f-0a47-4d93-b6e2-9f3a7c5d1e80


Response 200: an appointment. Errors: 404 APPOINTMENT_NOT_FOUND, 410 APPOINTMENT_ALREADY_CANCELLED, 422 APPOINTMENT_CANNOT_CANCEL.


Add Appointment Note


POST /appointments/{id}/notes. Body: content (1 to 5,000 characters). The text is added to the end of the appointment's notes, starting with [Zapier <time>].


Example request body:


{
  "content": "Client prefers minimal shading."
}


Response 200:


{
  "data": {
    "id": "2c8e5b1f-0a47-4d93-b6e2-9f3a7c5d1e80",
    "appointment_id": "2c8e5b1f-0a47-4d93-b6e2-9f3a7c5d1e80",
    "content": "Client prefers minimal shading.",
    "updated_at": "2026-03-01T12:00:00.000Z"
  }
}


Only the note you sent comes back. Earlier notes never do. Errors: 400 VALIDATION_FAILED, 404 APPOINTMENT_NOT_FOUND.


Create Client


POST /clients


Body field

Required

Notes

email

Yes


first_name, last_name

Yes

1 to 100 characters

phone

No

International format, for example +15551234567

artist_id

Shops: yes

Leave out on an artist connection

tags

No

Up to 20 tags, 40 characters each

referral_source

No

Up to 255 characters

preferred_contact

No

email, phone or app


Example request body:


{
  "email": "jane@example.com",
  "first_name": "Jane",
  "last_name": "Doe"
}


Response 201:


{ "data": { "client_id": "6a1d4c52-93be-4f07-8b2a-0c7e5d9f1a48", "email": "jane@example.com", "first_name": "Jane", "last_name": "Doe" } }


Errors: 400 VALIDATION_FAILED, 400 VALIDATION_REQUIRED_FIELD, 409 RESOURCE_ALREADY_EXISTS (a client with this email exists). Emits client.created.


Find or Create Client


POST /clients/find-or-create. Same body as Create Client except preferred_contact. Looks up the client by email. If none exists, creates one. If one exists and the name or phone you sent is different, updates it.


Example request body:


{
  "email": "jane@example.com",
  "first_name": "Jane",
  "last_name": "Doe"
}


Response 201 (created) or 200 (found):


{
  "data": {
    "client_id": "6a1d4c52-93be-4f07-8b2a-0c7e5d9f1a48",
    "created": false,
    "created_relationship": true,
    "email": "jane@example.com",
    "first_name": "Jane",
    "last_name": "Doe",
    "phone": "+15551234567"
  }
}


created_relationship is true when this artist had no earlier link to the client. Errors: 400, 404 RESOURCE_NOT_FOUND (the email belongs to someone who is not one of your clients).


Update Client


PATCH /clients/{id}. Send at least one of first_name, last_name, email, phone, tags (replaces all tags), is_vip, referral_source, preferred_contact, general_notes.


Example request body:


{
  "first_name": "Janet"
}


Response 200:


{ "data": { "client_id": "6a1d4c52-93be-4f07-8b2a-0c7e5d9f1a48", "first_name": "Janet", "last_name": "Doe", "email": "jane@example.com", "phone": "+15551234567" } }


Errors: 400 VALIDATION_FAILED, 404 RESOURCE_NOT_FOUND.


Add Client Note


POST /clients/{id}/notes. Body: content (1 to 5,000 characters), and artist_id (optional for shop connections, required for platform connections). The text is added to the client's general notes with a time stamp.


Example request body:


{
  "content": "Prefers afternoon appointments."
}


Response 200, or 201 if this was the artist's first note for the client:


{ "data": { "client_id": "6a1d4c52-93be-4f07-8b2a-0c7e5d9f1a48", "note_added": true } }


Stored notes are never returned.


Send Notification


POST /notifications. Queues a message to one of your clients.


Body field

Required

Notes

recipient_id

Yes

The client's UUID

template_id

Yes

One of the four messages below

variables

Yes, in practice

Object of text values. A missing value leaves a blank in the message, so send every variable the template uses (table below).

channels

No

Array of email, sms, push, in_app. Defaults to the template's channels. A channel the template does not support returns 400 VALIDATION_INVALID_ENUM. message (Custom message) has no email version: use sms, push or in_app. The other three also support email.


template_id

Variables to send

Optional, used by the email or text

appointment_scheduled

artist_name, appointment_date, appointment_time

appointment_location, appointment_type, manage_url, dashboard_url, client_first_name

appointment_confirmed

artist_name, appointment_date, appointment_time, and confirmed_by_artist set to true (without it the message is blank)

appointment_location

appointment_cancelled

artist_name, appointment_date, appointment_time

cancelling_party_name

message

sender_name, message_preview



The dates and times are text. In-app, push and text messages show them as you send them. The email uses Apprentice's saved email template, so it also reads the optional variables above. Any variable you leave out shows as a blank.


Example request body:


{
  "recipient_id": "6a1d4c52-93be-4f07-8b2a-0c7e5d9f1a48",
  "template_id": "message",
  "channels": [
    "in_app"
  ],
  "variables": {
    "sender_name": "Alex",
    "message_preview": "Please check your booking details."
  }
}


Response 202:


{ "data": { "queued": true, "template_id": "appointment_scheduled", "recipient_id": "6a1d4c52-93be-4f07-8b2a-0c7e5d9f1a48" } }


queued means accepted, not delivered. Delivery follows the client's own notification settings. The API also knows other message types, for example appointment_reminder and appointment_rescheduled. They are not offered here because Apprentice builds them from its own appointment data. A reminder is dropped without any error unless its variables name one of your appointments for that client. A reschedule email needs the appointment's saved time zone and its old and new start times, and fails inside the queue while the call still returns 202. Errors: 400 VALIDATION_FAILED, 400 VALIDATION_INVALID_ENUM (a channel the template does not support), 404 NOTIFICATION_TEMPLATE_NOT_FOUND, 422 NOTIFICATION_RECIPIENT_INVALID (not your client).


Create Booking Request


POST /booking-requests. Creates a pending request for the artist to review. It does not reserve calendar time and does not charge a deposit. The client receives a verification email and the artist receives a notification. Emits booking.requested.


Body field

Required

Notes

booking_page_id

Yes

UUID. The page decides the artist, location and service.

client_name

Yes

Up to 255 characters

client_email

Yes


description

Yes

Up to 5,000 characters

client_phone

No

Required if transactional_sms_opt_in is true

preferred_datetime

No

A time the client really chose, ISO 8601 with a time zone. Do not send the form submission time.

preferred_dates_text

No

Free text, up to 1,000 characters

preferred_date_range_start, preferred_date_range_end

No

YYYY-MM-DD

placement, size_estimate, style, color_scheme, budget_range

No

Text

has_tattoos

No

Boolean

reference_images

No

Up to 10 public image URLs. The links are saved. The files are not copied.

custom_fields

No

Object of up to 20 text answers

transactional_sms_opt_in

No

true only if the client actively agreed to booking-related texts. Default false.

sms_consent_captured_at

No

When they agreed. Defaults to the time Apprentice receives the request.


A phone number alone never gives text consent. This consent covers booking-related texts only, not marketing.


Example request body:


{
  "booking_page_id": "4e7a9c21-6d38-4b15-a0f2-8c1b3d5e7f90",
  "client_name": "Jane Doe",
  "client_email": "jane@example.com",
  "description": "A small botanical design on my forearm."
}


Response 201:


{
  "data": {
    "request_id": "7b3d1f95-2e48-4a60-91c7-5d0e8a2b4c16",
    "status": "pending",
    "service_type_slug": "tattoo",
    "preferred_dates_text": "Weekday afternoons in September",
    "verification_required": true,
    "booking_page_id": "4e7a9c21-6d38-4b15-a0f2-8c1b3d5e7f90",
    "booking_page_title": "Creative Consultations",
    "client_name": "Jane Doe",
    "client_email": "jane@example.com",
    "transactional_sms_opt_in": false
  }
}


Errors: 400 VALIDATION_FAILED, 404 RESOURCE_NOT_FOUND (page missing, inactive or not yours).


Book Appointment From Booking Page


POST /booking-submissions. Books a confirmed appointment at an exact date and time. It uses the same checks as the public booking page: availability, buffers, daily limits and conflicts. Emits booking.submitted.


Body field

Required

Notes

booking_page_id

Yes

UUID

client_name, client_email

Yes


requested_date

Yes

YYYY-MM-DD, in the booking location's time zone

requested_time

Yes

HH:MM, 24 hour, in the booking location's time zone

description

Yes

Up to 5,000 characters

client_phone

No


service_type_slug

No

A service the page offers. Defaults to the page's default service.

placement

Some services

Body location. Required for Laser Removal and Implants.

size_estimate, budget_range

No

Text

reference_images

No

Up to 10 URLs

booking_policy_acceptance

If the page has a policy

{ "versionId": "<booking_policy_version_id>", "accepted": true }

scarificationTechnique, scarificationHealingIntent

Scarification

See Scarification


This action cannot book a page that requires a deposit. Use Create Booking Request instead.


A consultation page (phone or in person) books here too. The page sets the format, the length and the offered times, and a time it does not offer returns 409 APPOINTMENT_CONFLICT. A phone consultation needs client_phone. Send no tattoo details. The booking never joins an existing project: the artist sees it as a new consultation inquiry. You get 400 VALIDATION_FAILED if the consultation needs a deposit or a signed waiver, because this action can collect neither. Share the booking page with the client instead.


If the page has a booking policy, show the client booking_policy_text from the booking page list. Send booking_policy_acceptance only after they agree. Without it you get 400 VALIDATION_FAILED, "Please review and accept the current booking policy before booking.", with details.policyDecision set to booking_policy_acceptance_required.


Example request body:


{
  "booking_page_id": "4e7a9c21-6d38-4b15-a0f2-8c1b3d5e7f90",
  "client_name": "Jane Doe",
  "client_email": "jane@example.com",
  "requested_date": "2026-11-10",
  "requested_time": "10:00",
  "description": "A small botanical design on my forearm."
}


Response 201:


{
  "data": {
    "appointment_id": "2c8e5b1f-0a47-4d93-b6e2-9f3a7c5d1e80",
    "submission_id": "5b8d0f12-6a34-4c97-8e21-1f9a3c7d5e40",
    "client_id": "6a1d4c52-93be-4f07-8b2a-0c7e5d9f1a48",
    "is_new_client": false,
    "start_time": "2026-03-01T14:00:00.000Z",
    "end_time": "2026-03-01T16:00:00.000Z",
    "management_url": "https://app.useapprentice.com/manage/sample-token",
    "client_name": "Jane Doe",
    "client_email": "jane@example.com",
    "artist_name": "Alex Smith",
    "location_name": "Downtown Studio",
    "service_type_slug": "tattoo",
    "service_display_name": "Tattoo",
    "service_summary_line": "Tattoo",
    "service_payload": {}
  }
}


management_url is a private link where the client can view or change the appointment. Treat it like a password.


Errors: 400 VALIDATION_FAILED (the message says what to fix), 404 RESOURCE_NOT_FOUND, 409 APPOINTMENT_CONFLICT ("Requested time slot is not available"), 422 SUBSCRIPTION_UPGRADE_REQUIRED ("This booking cannot be completed. Please contact the artist.").



Searches and dropdowns


Searches return a page of results. A search with no match returns 200 with "data": [], never an error. All use cursor pagination.


Zapier step

Method and path

Scope

Find Appointment

GET /appointments/search

appointments:read

Find Client

GET /clients/search

clients:read

Find Location, and the Location dropdown

GET /locations

locations:read

Find Booking Page, and the Booking page dropdown

GET /booking-pages

booking_pages:read


Find Appointment


GET /appointments/search. Newest start time first. Filters combine with AND.


Example request (with your Bearer header):


GET /appointments/search?status=confirmed&limit=20


Query parameter

Notes

status

One of the appointment statuses

start_date

Only appointments that start at or after this time

end_date

Only appointments that start at or before this time

client_id

UUID. Only this client's appointments.

cursor, limit

Pagination


Response: { "data": [appointment, ...], "pagination": {...} }. See the appointment object. Errors: 400 VALIDATION_INVALID_FORMAT (bad client_id or cursor).


Find Client


GET /clients/search. Newest client first.


Example request (with your Bearer header):


GET /clients/search?email=jane%40example.com&limit=20


Query parameter

Notes

email, phone

Partial match, not case sensitive

name

Partial match on first or last name

cursor, limit

Pagination


Each result:


{
  "id": "6a1d4c52-93be-4f07-8b2a-0c7e5d9f1a48",
  "first_name": "Jane",
  "last_name": "Doe",
  "email": "jane@example.com",
  "phone": "+15551234567",
  "avatar_url": null,
  "status": "active",
  "member_since": "2026-01-12T10:00:00.000Z",
  "total_appointments": 5,
  "artist_id": "3f1c2a9e-5b7d-4e0a-9c11-7d2f6a8b4e10"
}


Locations


GET /locations. Your active locations, newest first. Optional query name returns only locations whose name contains that text (case-insensitive; blank is ignored; asterisks are ignored; only the first 100 characters are used). Each has id, location_name, timezone (IANA), address_line1, address_line2, city, state, postal_code, country and is_active. Other fields may be present and are not part of this contract.


Example request (with your Bearer header):


GET /locations?limit=20


{
  "data": [
    {
      "id": "5d9b2e71-4a06-4c83-b1f7-0e8a3c6d2f95",
      "location_name": "Downtown Studio",
      "timezone": "America/New_York",
      "address_line1": "123 Main St",
      "city": "New York",
      "state": "NY",
      "postal_code": "10001",
      "is_active": true
    }
  ],
  "pagination": { "nextCursor": null, "hasMore": false, "limit": 20, "returned": 1 }
}


Booking pages


GET /booking-pages. Your active booking pages, newest first. Optional query title returns only booking pages whose title contains that text (case-insensitive; blank is ignored; asterisks are ignored; only the first 100 characters are used). Each has id, title, slug, is_active, duration_minutes, default_service_type_slug and location_id. Other fields may be present and are not part of this contract.


Example request (with your Bearer header):


GET /booking-pages?limit=20


If a page has a booking policy, these fields carry it. Otherwise they are null.


Field

Meaning

booking_policy_version_id

The current policy version

booking_policy_text

The terms to show the client

booking_policy

versionId, ownerType, ownerId, noticeHours, maxReschedules, depositNonrefundable, text


{
  "id": "4e7a9c21-6d38-4b15-a0f2-8c1b3d5e7f90",
  "title": "Custom Tattoo Consultation",
  "slug": "custom-tattoo",
  "is_active": true,
  "duration_minutes": 120,
  "booking_policy_version_id": null,
  "booking_policy_text": null,
  "booking_policy": null
}



Services with extra rules


service_type_slug is one of: tattoo, piercing, pmu (permanent makeup), laser_removal, scarification, tooth_gem, implants.


Scarification


Scarification needs two answers. They drive the consent review and the aftercare instructions.


Field

Values

scarificationTechnique

cutting, branding, ink_rubbing, skin_peel, not_sure

scarificationHealingIntent

keloid_preferred (a raised scar), flat_preferred (a flatter scar), no_preference


  • Create Appointment: required for Scarification. If you link a project_id whose stored answers already include both, you can leave them out. Values you send win over the project's.
  • Book Appointment From Booking Page: required when service_type_slug is scarification. If you leave service_type_slug out and the page's default service is Scarification, they are required too. For other services they are ignored.
  • A missing or wrong value returns 400 VALIDATION_FAILED: "Scarification needs a technique and healing intent: scarificationTechnique is required; ...". For a wrong value the message lists the allowed values.


Laser removal and implants


placement (the body location to treat) is required when booking Laser Removal or Implants from a booking page. Without it: 400 VALIDATION_FAILED, "placement is required for Laser Removal: the body location to treat."


When a service is refused


Laser removal, scarification and implants follow the artist's consent timing. This applies to Book Appointment From Booking Page.


  • If the artist collects a signed consent form at booking, the request is refused. An integration cannot collect a signature. The error is 400 VALIDATION_FAILED: "The artist requires a signed Scarification consent form before the appointment is booked, and Zapier can't collect a signature. Share the booking page with the client instead." Use Create Booking Request, or send the client to the booking page.
  • If the form is sent after booking (in the prep step), the booking goes through. Apprentice first runs the same live checks as the booking page: the artist must offer the service, and the shop's policy and required forms must be in place. If the service cannot be booked right now you get 400 VALIDATION_FAILED: "Scarification isn't available to book right now."
  • If Apprentice cannot confirm the consent settings, it returns 500 SERVER_DATABASE_ERROR and nothing is booked. Try again.


Create Booking Request never needs a signature. It makes a pending request the artist reviews.



Errors


HTTP status and error.code. Codes can be added over time, so handle unknown ones by status.


Status

Code

When

400

VALIDATION_FAILED

A field is missing or wrong, or a rule was broken. message says which.

400

VALIDATION_REQUIRED_FIELD

A required field is missing for this kind of connection

400

VALIDATION_INVALID_UUID, VALIDATION_INVALID_FORMAT, VALIDATION_INVALID_DATE, VALIDATION_INVALID_ENUM, VALIDATION_INVALID_URL, VALIDATION_OUT_OF_RANGE

A value has the wrong form or is out of range

401

AUTH_REQUIRED

No Authorization: Bearer header

401

AUTH_TOKEN_INVALID

The token is malformed, from an unknown client, or has no active connection. Refresh and retry.

403

OWNER_SCOPE_VIOLATION

The connection does not have the scope this call needs

403

AUTH_INSUFFICIENT_PERMISSIONS, AUTH_FORBIDDEN

The person who connected can no longer do this, or the shop does not allow exporting that client

404

RESOURCE_NOT_FOUND, APPOINTMENT_NOT_FOUND, NOTIFICATION_TEMPLATE_NOT_FOUND

The thing does not exist, or is not yours

409

APPOINTMENT_CONFLICT

The time overlaps another appointment

409

RESOURCE_ALREADY_EXISTS, CONFLICT

The record already exists, or cannot be reused

409

IDEMPOTENCY_REQUEST_IN_PROGRESS

Same Idempotency-Key is still running. Retry after Retry-After.

409

GUARDIAN_VERIFICATION_REQUIRED

A minor's appointment needs guardian verification first

410

APPOINTMENT_ALREADY_CANCELLED


422

APPOINTMENT_ALREADY_CONFIRMED, APPOINTMENT_CANNOT_CANCEL, BUSINESS_LOGIC_VIOLATION, NOTIFICATION_RECIPIENT_INVALID, SUBSCRIPTION_UPGRADE_REQUIRED

The request is valid but the current state does not allow it

429

RATE_LIMIT_EXCEEDED

See Rate limits. Wait Retry-After seconds.

500

SERVER_DATABASE_ERROR, SERVER_CONFIGURATION_ERROR

Something failed on our side. Safe to retry.

503

INTEGRATION_DISABLED, IDEMPOTENCY_UNAVAILABLE

The integration is paused, or the safety check was unavailable. Retry-After says when to retry.



What the API does not return


To keep health and consent information out of automation tools:


  • Waivers. consent.signed carries only IDs and the time. Waiver text, answers and signatures are never sent or returned.
  • Health answers. In service_payload, answers that hold health information, such as a tooth gem client's metal allergy or dental work, are removed everywhere: events, actions and searches.
  • Guardian and age data. Appointment responses do not include whether a client is a minor, their age, or guardian consent status.
  • Notes. Appointment and client notes are write-only. Responses to the note actions return only what you just sent. Appointment responses do not include the notes field.
  • Secrets and money. Access tokens and verification tokens are never returned. Appointment, client and event data carry no pricing or payment details, and the Zapier integration has no payment steps. Two things do return more: a booking page in the booking page list includes the page's own settings, which can include its deposit settings (these are your settings, not a client's), and Book Appointment From Booking Page returns management_url, a private link for the client. Treat that link like a password.


Free text you put in a field, for example a note or a booking description, is stored as you send it. Do not send health details through an automation you do not need to.



Changelog


The integration version is separate from the API version. Full notes are in the integration's changelog.


Version

Date

Summary

Documentation

2026-10-12

Added endpoint index, OAuth and request examples, hook maintenance responses, and working section links.

2.0.2

2026-10-08

Find Location and Find Booking Page take an optional name or title filter. DELETE /hooks/{id} for an unknown subscription returns 404 instead of 200.

2.0.1

2026-10-06

Fixes: Send Notification choices that failed, optional Artist on Create Appointment, location dropdown labels, output fields that did not match responses, readable error messages. Removed the unused Note Type input. Appointment responses use a fixed field list.

2.0.0

2026-10-02

Removed Create Payment Link, Find Payment and Payment Status Changed. Breaking for Zaps that used them.

1.3.1

2026-10-01

Scarification fields optional when Service Type is blank on Book Appointment From Booking Page.

1.3.0

2026-10-01

Scarification technique and healing intent. Hardened services follow the artist's consent timing.

1.2.1

2026-08-24

SMS consent fields on Create Booking Request.

1.2.0

2026-08-24

Added Create Booking Request. Renamed the older booking action to Book Appointment From Booking Page.

1.1.0

2026-05-21

Service fields on appointment and booking events.

1.0.0

2026-02-12

First release.



Tips


  • Test with your own booking page and a client email you control.
  • Use Idempotency-Key on every write you might retry.
  • Store the newest refresh token every time you refresh.
  • Keep signing_secret private. It lets anyone forge events to your URL.


Common Issues


  • Issue: 401 AUTH_TOKEN_INVALID after a while: Solution: The access token expired. Refresh it. If refresh fails, connect again.
  • Issue: 403 OWNER_SCOPE_VIOLATION: Solution: The connection is missing a scope. Remove the connection and connect again.
  • Issue: 409 APPOINTMENT_CONFLICT: Solution: Pick another time, or check the artist's calendar.
  • Issue: A trigger stopped firing: Solution: After 10 failed deliveries in a row the subscription is disabled. Turn the Zap off and on again, or call PATCH /hooks/{id}.
  • Issue: Scarification, laser removal or implant booking is refused: Solution: See When a service is refused. Use Create Booking Request.





Need more help? Contact support at help.useapprentice.com

Updated on: 13/10/2026

Was this article helpful?

Share your feedback

Cancel

Thank you!