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/zapierVersion. 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 |
| Valid token | |
POST |
|
| |
DELETE |
|
| |
GET |
|
| |
PATCH |
|
| |
POST |
|
| |
PATCH |
|
| |
POST |
|
| |
DELETE |
|
| |
POST |
|
| |
GET |
|
| |
POST |
|
| |
POST |
|
| |
PATCH |
|
| |
POST |
|
| |
GET |
|
| |
POST |
|
| |
POST |
|
| |
POST |
|
| |
GET |
|
| |
GET |
|
|
Contents
- Authentication
- Shared behavior
- Events (triggers)
- Actions
- Searches and dropdowns
- Services with extra rules
- Errors
- What the API does not return
- 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 |
|
Token URL |
|
Refresh URL | Same as the token URL |
Grant types |
|
PKCE | Yes. Method |
Client authentication | HTTP Basic header with the client ID and secret. Not in the body. |
Token type |
|
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
- Send the user to the authorize URL with
client_id,response_type=code,redirect_uri,state,code_challengeandcode_challenge_method=S256. - 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.
- Apprentice redirects to your
redirect_uriwith acodeand yourstate. - Exchange the code with a
POSTto the token URL. The body isapplication/x-www-form-urlencoded:grant_type=authorization_code,code,redirect_uri,code_verifier. - 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 |
|---|---|
| Find appointments |
| Create, update, confirm and cancel appointments |
| Find clients |
| Create, find or create, and update clients |
| Add notes to appointments and clients |
| Send notifications to clients |
| Create booking requests and book from a booking page |
| List booking pages |
| List locations |
| Subscribe to and unsubscribe from events |
| 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
409with 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_PROGRESSwithRetry-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 |
|---|---|
| Page size, 1 to 100. Default 20. |
| The |
{
"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 |
|---|---|
| The limit for the window |
| Requests left in the window |
| When the window resets, in Unix seconds |
| 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_urlmust 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
eventreturns400 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 |
|
|---|---|---|
New Appointment |
|
|
Appointment Status Changed |
|
|
New Client Profile |
|
|
New Scheduled Booking |
|
|
New Booking Request |
|
|
Waiver Signed |
|
|
Client Checked In |
|
|
Envelope
Every event has the same outer shape. The event details are in data.
Field | Meaning |
|---|---|
| Event ID (UUID) |
| Event name, for example |
|
|
| When the event was created |
| See the table above |
| ID of the thing the event is about |
| The payload, below |
Webhook delivery
Apprentice sends each event as an HTTPS POST with Content-Type: application/json and these headers:
Header | Value |
|---|---|
| The event name |
| A unique delivery ID. Use it to ignore repeats. |
|
|
|
|
|
|
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
2xxresponse 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 |
|---|---|---|
| string | Appointment ID |
| string | |
| string | |
| string | For example |
| string | The kind of session: |
| string or null | |
| string |
|
| string | For example |
| string | Short label for lists |
| object | Service details. |
| 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 |
|---|---|---|
| string | Booking submission ID |
| string | |
| string | |
| string | |
| Same as appointments | |
| 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 |
|---|---|---|
| string | Request ID |
| string | |
| string | Always |
| string | |
| string | |
| boolean | Always |
| Same as appointments | |
| 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 |
|
|
Update Appointment |
|
|
Confirm Appointment |
|
|
Cancel Appointment |
|
|
Add Appointment Note |
|
|
Create Client |
|
|
Find or Create Client |
|
|
Update Client |
|
|
Add Client Note |
|
|
Send Notification |
|
|
Create Booking Request |
|
|
Book Appointment From Booking Page |
|
|
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 |
|---|---|---|
| string (UUID) or null | |
| string | |
| string |
|
| string |
|
| string or null | |
| string or null | |
| string or null | |
| string or null | |
| string or null | |
| 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 |
|---|---|---|
| Yes | UUID of one of your clients |
| Yes | ISO 8601 date and time |
| Shops: yes | UUID. Leave out on an artist connection to book on your own calendar. An artist connection can only name itself. |
| No | Defaults to the artist's primary location |
| No | An existing project for this client and artist. A new project is created if you leave it out. |
| No | |
| No |
|
| No | See service values. Default |
| No | |
| No | Default |
| 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/confirmResponse 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-9f3a7c5d1e80Response 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 |
|---|---|---|
| Yes | |
| Yes | 1 to 100 characters |
| No | International format, for example |
| Shops: yes | Leave out on an artist connection |
| No | Up to 20 tags, 40 characters each |
| No | Up to 255 characters |
| No |
|
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 |
|---|---|---|
| Yes | The client's UUID |
| Yes | One of the four messages below |
| 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). |
| No | Array of |
| Variables to send | Optional, used by the email or text |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
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 |
|---|---|---|
| Yes | UUID. The page decides the artist, location and service. |
| Yes | Up to 255 characters |
| Yes | |
| Yes | Up to 5,000 characters |
| No | Required if |
| No | A time the client really chose, ISO 8601 with a time zone. Do not send the form submission time. |
| No | Free text, up to 1,000 characters |
| No |
|
| No | Text |
| No | Boolean |
| No | Up to 10 public image URLs. The links are saved. The files are not copied. |
| No | Object of up to 20 text answers |
| No |
|
| 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 |
|---|---|---|
| Yes | UUID |
| Yes | |
| Yes |
|
| Yes |
|
| Yes | Up to 5,000 characters |
| No | |
| No | A service the page offers. Defaults to the page's default service. |
| Some services | Body location. Required for Laser Removal and Implants. |
| No | Text |
| No | Up to 10 URLs |
| If the page has a policy |
|
| 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 |
|
|
Find Client |
|
|
Find Location, and the Location dropdown |
|
|
Find Booking Page, and the Booking page dropdown |
|
|
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=20Query parameter | Notes |
|---|---|
| One of the appointment statuses |
| Only appointments that start at or after this time |
| Only appointments that start at or before this time |
| UUID. Only this client's appointments. |
| 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=20Query parameter | Notes |
|---|---|
| Partial match, not case sensitive |
| Partial match on first or last name |
| 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=20If a page has a booking policy, these fields carry it. Otherwise they are null.
Field | Meaning |
|---|---|
| The current policy version |
| The terms to show the client |
|
|
{
"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 |
|---|---|
|
|
|
|
- Create Appointment: required for Scarification. If you link a
project_idwhose 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_slugisscarification. If you leaveservice_type_slugout 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_ERRORand 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 |
| A field is missing or wrong, or a rule was broken. |
400 |
| A required field is missing for this kind of connection |
400 |
| A value has the wrong form or is out of range |
401 |
| No |
401 |
| The token is malformed, from an unknown client, or has no active connection. Refresh and retry. |
403 |
| The connection does not have the scope this call needs |
403 |
| The person who connected can no longer do this, or the shop does not allow exporting that client |
404 |
| The thing does not exist, or is not yours |
409 |
| The time overlaps another appointment |
409 |
| The record already exists, or cannot be reused |
409 |
| Same |
409 |
| A minor's appointment needs guardian verification first |
410 |
| |
422 |
| The request is valid but the current state does not allow it |
429 |
| See Rate limits. Wait |
500 |
| Something failed on our side. Safe to retry. |
503 |
| The integration is paused, or the safety check was unavailable. |
What the API does not return
To keep health and consent information out of automation tools:
- Waivers.
consent.signedcarries 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
notesfield. - 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. |
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-Keyon every write you might retry. - Store the newest refresh token every time you refresh.
- Keep
signing_secretprivate. It lets anyone forge events to your URL.
Common Issues
- Issue:
401 AUTH_TOKEN_INVALIDafter 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.
Related Articles
- Connecting Apprentice to Zapier
- Understanding Data Privacy on Apprentice
- Opting In/Out of SMS Notifications
Need more help? Contact support at help.useapprentice.com
Updated on: 13/10/2026
Thank you!
