A REST API over your ChiDesk account. Read the diary, catalogue and sales data, create and
change records, and subscribe to events. Everything is JSON over HTTPS.
Building something new? Use v2. Version 1 still works and will be retired
3 months after the 2026.8 release, so plan to move existing integrations across.
The v1 reference stays available here.
Overview
Every request goes to your own account's address. The account name is the subdomain, and
ChiDesk works out which account you mean from the host you called - there is no account id
to pass and no way to reach another account with your credentials.
Base URL
https://[account].chidesk.com/api/v2/
So for the account acme, the sites endpoint is
https://acme.chidesk.com/api/v2/sites. Paths in this reference
are written from /api/v2/ onwards; put your own account's address in front of them.
What every request needs
Requirements for every API request
Requirement
Detail
HTTPS
All calls must be over HTTPS.
Bearer token
An Authorization: Bearer <token> header on every call except the token endpoints themselves. See Authentication.
Named parameters
Every query parameter an endpoint declares must appear in the URL by name, even the optional-looking ones. See Conventions - this is the single most common reason a first call fails.
JSON
Request bodies are JSON with Content-Type: application/json. Responses are JSON.
UTC
Every date and time, in and out, is UTC.
Verify your setup
GET /api/v2/ping is the quickest way to confirm your account address, token
and network path all work before you write any real code. It returns the account ChiDesk
resolved your request to, so it also catches a wrong subdomain. It is not a data endpoint
and has no category of its own.
curl -X GET \
"https://acme.chidesk.com/api/v2/ping" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
Ask your ChiDesk administrator to create an API credential for your integration. You get a
client id and a secret. Exchange those for a bearer token,
then send the token on every call.
Get a token
POST /api/v2/token with the client id and secret in the JSON body. This endpoint
is the one place the secret is used; it is never sent on a normal call.
var credentials = new
{
clientId = "your-client-id",
secret = "your-client-secret"
};
var http = new HttpClient();
http.BaseAddress = new Uri("https://acme.chidesk.com/");
var response = await http.PostAsync(
"api/v2/token",
new StringContent(
JsonConvert.SerializeObject(credentials),
Encoding.UTF8,
"application/json"));
response.EnsureSuccessStatusCode();
var token = JObject.Parse(await response.Content.ReadAsStringAsync());
var accessToken = token["access_token"].ToString();
// Every sample in this reference assumes an http client set up like this.
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
The token to send on every subsequent call. Returned once and never again - ChiDesk stores only a hash of it, so if you lose it you must get a new one.
token_type
String
Always Bearer.
expires_in
Int32
Seconds until the token expires.
expires_utc
DateTime
The moment it expires, UTC.
Tokens last 6 months. They are opaque - do not try to read
anything out of them. Store the token, reuse it, and get a new one when it expires; do not
call the token endpoint before every request.
Use the token
Send it as a bearer token on every call to every other endpoint.
curl -X GET \
"https://acme.chidesk.com/api/v2/sites?state=1" \
-H "Authorization: Bearer zTn7Kq...redacted..."
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
Refresh and rotate
Four endpoints manage the credential itself. All four authenticate with the client id and
secret in the body, exactly like the token endpoint, and none of them takes a bearer token.
Credential lifecycle endpoints
Endpoint
What it does
POST/api/v2/token
Issues a token. Existing tokens keep working.
POST/api/v2/token/refresh
Revokes the credential's active tokens and issues one fresh token. Use this to roll a token deliberately, or if you think one has leaked. Anything still using the old token starts failing immediately.
POST/api/v2/token/rotate-secret
Issues a new secret and revokes active tokens. The new secret is returned once, in client_secret.
var response = await http.PostAsync(
"api/v2/token/refresh",
new StringContent(
JsonConvert.SerializeObject(credentials),
Encoding.UTF8,
"application/json"));
// Any token issued earlier stops working the moment this returns.
response.EnsureSuccessStatusCode();
Allow-lists on a credential
A credential can be limited to certain endpoints, certain IP addresses, or both. Both lists
are optional: an empty list means no restriction. Both are also matched loosely, and it is
worth understanding exactly how before you rely on either as a security control.
Endpoint allow-list
One path per line. A request is allowed when the path it called ends with one
of the lines - it is a suffix test, not an exact match. Two consequences matter in practice:
A line for a collection does not cover paths beneath it. api/v2/clients
allows /api/v2/clients but not /api/v2/clients/contacts, because
that path ends in contacts. Sub-routes need their own lines.
Routes that end in a record id cannot be listed, since the path ends with the id itself.
A credential with a non-empty endpoint allow-list will be refused
GET /api/v2/appointments/{appointmentId} even though
api/v2/appointments is on the list. If your integration reads or writes
individual records by id, leave the endpoint allow-list empty.
A blank line allows everything. The list is split on line breaks and
empty entries are kept, and every path "ends with" an empty string - so one stray blank
line, or a trailing newline that leaves an empty last entry, turns the whole allow-list
off without any visible change to it. If you are relying on this list, check there are
no empty lines in it.
Each category below lists the exact lines to add for the endpoints it documents.
IP allow-list
Security warning
The IP allow-list is matched as a substring of the stored list, not as a
list of exact addresses. An entry of 10.0.0.1 therefore also admits
110.0.0.12, 210.0.0.19 and any other address that happens to
contain those characters. Treat it as a coarse filter that narrows who can call you, not
as a boundary that proves who did. Keep the secret secret and rotate it if in doubt -
that is the control that actually holds.
Conventions
Read this one first
Every query parameter an endpoint declares must be present in the URL by name -
including the nullable and optional-looking ones. ChiDesk picks the code that
handles your request by matching the parameter names you sent. Leave one out and nothing
matches - and you get a routing error rather than a message telling you what was missing.
Send the name with an empty value when you do not want to filter on it:
&stateId=&find=&clientId=.
Which routing error you see depends on the path, not on what you did wrong. If another
method is registered on the same path - POST /api/v2/appointments alongside
the GET, for instance - the path still exists once your request is ruled out,
so the answer is 405. Where the path has only the one action, such as
GET /api/v2/packages, you get 404. Treat 404 and
405 as the same signal: a parameter name is missing.
cURL
# Resolves. Every parameter is present by name, three of them empty.
curl "https://acme.chidesk.com/api/v2/appointments?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&stateId=&start=2026-08-01%2000:00&end=2026-08-08%2000:00&find=&clientId=" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
# 405. stateId, find and clientId are missing, so this action does not match. The path
# itself still exists for POST, which is why the server answers 405 and not 404.
curl "https://acme.chidesk.com/api/v2/appointments?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&start=2026-08-01%2000:00&end=2026-08-08%2000:00" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
Each endpoint's parameter table marks which parameters this applies to. Where a parameter is
listed as Optional it carries a default and really can be
left out entirely - that is the exception, and it is called out where it is true.
Where the record id goes
The shipped API is not consistent about this: some update endpoints take the record id in the
path, others take it as a query parameter. There is no rule to infer - check the endpoint.
We have documented it as it behaves rather than quietly changing it, because changing it
would break the integrations already calling it.
Where the record id is supplied on update endpoints
A ChiDesk account can have several sites, and nearly every collection endpoint is
scoped to one of them with a siteId. Two are account-wide and take no
siteId: GET /api/v2/sites, which is how you discover the ids in
the first place, and GET /api/v2/reports/sales, where siteId is
an optional filter rather than a requirement. Start by listing your sites and keep the ids.
Endpoints that name a single record - /clients/{id},
/vouchers/{voucherId}, /accountingdocuments/{accountingDocumentId}
and the rest - take no siteId either, and they are not restricted to one site:
an id is unique across the whole account, so any record on the account can be read or
changed through them. Restricting an integration to a single site's data is therefore not
something these endpoints do for you. The same is true of
PUT /api/v2/clients and PUT /api/v2/classes/classbookings, which
identify the record by id and take no site at all.
Dates, ids and field names
Value conventions
Convention
Detail
Time zone
Every date and time is UTC, both in requests and in responses. Convert in your own code, using the site's time zone from GET /api/v2/sites/{id} if you need local times.
Date format
yyyy-MM-dd HH:mm. Remember to URL-encode the space in a query string.
New records
Use an empty Guid - 00000000-0000-0000-0000-000000000000 - for the primary key of anything you are creating.
Array parameters
Repeat the name once per value: services=<guid>&services=<guid>.
Field names
JSON fields are PascalCase, matching the object schemas. The token endpoints, the WebHook register body and the whole ClassPass category are the exceptions - they use snake_case, and are marked where they appear.
itemState is not a wildcard
Wherever a list endpoint takes itemState (or state, or
classType), the value is matched exactly against the
record. So Active returns active records and Inactive
returns inactive ones - but All does not return both. It looks for a
state of 0, which no record has, and returns an
empty list with a 200. Ask for one state at a time,
and make two calls if you need both.
Errors
There are two different error shapes in this API, and which one you get depends on which
endpoint you called. They are not interchangeable - if your error handling only understands
one of them it will misread the other.
Authorization failures: 403 with a plain-text body
It is 403, not 401
Every authorization failure on a business endpoint returns
403 Forbidden with a plain-text body - even
a missing or expired token, where most APIs would return 401. There is no
WWW-Authenticate header and no JSON. Client libraries that watch for a
401 to trigger a re-authentication will never fire, and ones that expect JSON
will fail to parse the body. Handle 403.
The body is the word Unauthorized, then a dash, then the reason. The reasons are
fixed strings, so you can match on them:
Reasons returned with a 403
Body
What went wrong
Unauthorized - No bearer token sent in request header.
The Authorization header was missing or empty.
Unauthorized - Invalid or expired token.
The token is not recognised, has been revoked by a refresh or rotate, or has passed its expiry. Get a new one.
Unauthorized - Invalid client IP address.
The credential has an IP allow-list and your address did not match it.
Unauthorized - Invalid end-point.
The credential has an endpoint allow-list and the path you called did not match any line. Check the suffix-matching rules in Allow-lists on a credential - this is the usual cause when a call to a record id is refused.
Unauthorized
No reason given. Something failed while checking the credential.
Token endpoints: JSON with an error code
The token and rotate endpoints do not use the shape above. They return JSON with a single
error field, and they do use 401.
No body was sent, or it could not be read as JSON.
401
{ "error": "invalid_client" }
The client id or secret is wrong, or the credential is not active. The response deliberately does not say which.
500
{ "error": "server_error" }
Something failed while issuing the token. Nothing about the credential is ever echoed back in an error.
Validation and other statuses
Other statuses returned by business endpoints
Status
When
400
A body was required and not sent, a field failed validation, or a documented limit was exceeded - for example a date range wider than the endpoint allows. The body carries the reason, and validation messages are in the account's own language, so match on the status rather than the text.
404
Usually not a missing record - it means no endpoint matched the request. Nine times out of ten a query parameter was left out; see Conventions.
405
The same problem as 404, on a path that also serves another method. Your request did not match an action, but the path itself still exists - so read it as a missing query parameter, not as "this endpoint does not accept this method". See Conventions.
409
The record already exists. Only POST /api/v2/webhooks uses this.
Several endpoints report failure, or do nothing at all, inside a 200. Each
one is called out where it appears, but they are worth knowing about before you write
your error handling:
POST /api/v2/appointments/validate
returns the items that are no longer available in the body of a 200. An
empty body is the success case.
GET /api/v2/webhooks/poll returns an empty
body for an event name it does not recognise, so a typo looks like "no data".
POST and
PUT /api/v2/classes/classbookings
accept a booking and write nothing when its ObjectState is left at the
default, and echo back the id you sent either way.
The ClassPass attendance endpoint reports
CANCELLED for an appointment id that does not exist, rather than
reporting that it does not exist.
Any list endpoint taking itemState returns an empty array rather than
an error when you ask for All - see
Dates, ids and field names.
Rate limits
The API allows 4 requests per second. The request that
goes over the line is rejected with 429 Too Many Requestsand starts a
two-minute lockout, during which every further call to that endpoint is
rejected immediately. Going over the limit therefore costs you two minutes, not one request -
this is the part that catches people out, and V1 never mentioned it.
Rate limit values
Setting
Value
Requests allowed
4 per 1 second
Lockout once exceeded
120 seconds
Counted per
Endpoint, per calling IP address. Different endpoints have separate budgets, so a burst against one does not lock you out of another.
Window
Fixed, not sliding. The one-second window opens on your first request and closes a second later regardless of what happens in between.
Response
HTTP/1.1 429 Too Many Requests
Content-Type: application/json; charset=utf-8
"Your request has been throttled. Please try again in 2 minutes."
Changed from v1
The v1 page published 8 requests per second. That is not the figure the API enforces -
it belongs to a different throttle. Size your integration for
4 per second.
In practice: pull in batches rather than record by record, put a small delay between calls in
any loop, and back off for two minutes rather than retrying immediately when you do see a
429. Where an endpoint accepts a date range or a record count, ask for a wide one
once instead of many narrow ones.
Appointments
Read the diary, find bookable times, and create, re-state or cancel appointments. This is the largest category in the API and the one most integrations start with.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
All six query parameters must appear in the URL by name, including the ones that look optional. Send them empty (stateId=&find=&clientId=) when you do not want to filter on them - omitting a name entirely means the request never reaches this action. Verified against a running server: you get a 405, not a 404, because POST /api/v2/appointments is registered on the same path, so the path still exists for another method once the GET is ruled out.
The range is capped at 90 days. A wider range returns 400 with the message "Maximum date range is 90 days".
Parameters
Parameters for GET /api/v2/appointments
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose diary to read.
stateId
Guid
query
Required
Return only appointments in this state. Send empty for every state.
start
DateTime
query
Required
Start of the range, UTC.
end
DateTime
query
Required
End of the range, UTC. At most 90 days after start.
find
String
query
Required
Free-text filter on client or service name. Send empty for no filter.
clientId
Guid
query
Required
Return only this client's appointments. Send empty for every client.
Re-check that every item in a cart is still available. Call this immediately before creating an appointment.
A cart that is no longer bookable still returns 200 - not a 4xx. Success is an empty response body; a failure is a 200 whose body is a comma-separated list of the items that went away, for example "Swedish Massage 60min is no longer available". Treat any non-empty body as a failure.
The body is a bare JSON array of AppointmentService objects.
var content = new StringContent(
JsonConvert.SerializeObject(cart), Encoding.UTF8, "application/json");
var response = await http.PostAsync(
"api/v2/appointments/validate?siteId=" + siteId, content);
response.EnsureSuccessStatusCode();
// A 200 does not mean the cart is bookable - check the body.
var message = await response.Content.ReadAsStringAsync();
if (!String.IsNullOrWhiteSpace(message))
{
// message lists the items that are no longer available.
}
Send AppointmentId in the body as well as in the route. The record that gets loaded and saved is the one named by the body's AppointmentId, while the response echoes the id from the route - so if the two disagree you will update one appointment and be told about another. Keep them identical.
This is one of the endpoints that takes its id in the route. Several others take it in the query string instead; see Conventions.
Parameters
Parameters for PUT /api/v2/appointments/{appointmentId}
Name
Type
In
Required
Description
appointmentId
Guid
route
Required
The appointment to update. Must match AppointmentId in the body.
// The body's AppointmentId decides which record is saved - keep it equal to the route id.
var content = new StringContent(
JsonConvert.SerializeObject(appointment), Encoding.UTF8, "application/json");
var response = await http.PutAsync(
"api/v2/appointments/" + appointment.AppointmentId, content);
response.EnsureSuccessStatusCode();
var response = await http.DeleteAsync(
"api/v2/appointments/" + appointmentId);
response.EnsureSuccessStatusCode();
Clients
Read, search, create and update client records, check whether a client already exists before creating a duplicate, pull a client's appointment history, and verify a client's own account password.
A client whose record is not tied to a single site belongs to the whole account, and is returned by every site's list. So the same client can appear under more than one siteId - de-duplicate on ClientId, not on the pair.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
Only siteId is required. The other three carry defaults and may be left out of the URL altogether - unlike GET /api/v2/appointments, where every name has to be present.
find is a starts-with match, not a contains match, and it is tried against first name, last name, "first last", client code, e-mail, mobile number and membership number. Searching for "smith" finds Smith but not Smithers-Jones under a first name of Anne; searching for "ann" finds Anne.
creationDate reads as a single day but behaves as a from-date: clients created on or after that moment are returned. Use it to poll for new clients since your last sync.
itemState is not a wildcard - see Conventions. Ask for Active or Inactive; All returns nothing.
Parameters
Parameters for GET /api/v2/clients
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose clients to read. Account-wide clients are included as well.
itemState
ItemStates
query
Optional
Active (1, the default) or Inactive (2). Names and numbers are both accepted.
find
String
query
Optional
Starts-with filter over name, code, e-mail, mobile and membership number. Omit for no filter.
creationDate
DateTime
query
Optional
Return only clients created on or after this moment, UTC.
curl -X GET \
"https://acme.chidesk.com/api/v2/clients?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&itemState=Active&creationDate=2026-07-01%2000:00" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
// Poll for clients created since the last successful sync.
var url = "api/v2/clients"
+ "?siteId=" + siteId
+ "&itemState=Active"
+ "&creationDate=" + lastSyncUtc.ToString("yyyy-MM-dd HH:mm");
var response = await http.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
Fetch one client in full, including addresses, notes and account details.
This route takes no siteId. The client is found by its id alone, so a client at any site on the account can be read here - a list restricted to one site does not restrict this.
Look up a client by e-mail address. Call this before creating a client so you do not make a duplicate.
The response body is the matching client's id on its own, or the literal null - not an object, and not a 404. Read it as a nullable Guid.
The comparison is case-insensitive and surrounding whitespace is trimmed. Only Active clients are considered, and only ones with an e-mail address recorded.
If two clients share an address you get the first one, with no indication that there was more than one.
Parameters
Parameters for GET /api/v2/clients/emailexists
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site to search, plus the account-wide clients.
email
String
query
Required
The e-mail address to look for.
Responses
Responses for GET /api/v2/clients/emailexists
Status
Meaning
200
The client's id as a bare JSON string, or null when no Active client has that address.
403
Authorization failed. See Errors.
Example
curl -X GET \
"https://acme.chidesk.com/api/v2/clients/emailexists?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&email=jo%40example.com" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
# 200 "7e8f9012-3a4b-4c5d-6e7f-8091a2b3c4d5" - found
# 200 null - no such client
var url = "api/v2/clients/emailexists"
+ "?siteId=" + siteId
+ "&email=" + Uri.EscapeDataString(email);
var response = await http.GetAsync(url);
response.EnsureSuccessStatusCode();
// The body is a bare guid or the literal null.
var body = await response.Content.ReadAsStringAsync();
Guid? existingClientId = JsonConvert.DeserializeObject<Guid?>(body);
Check a client's own account password, for signing a client in to a portal or app you have built.
Security warning
This endpoint takes the client's password as a query-string parameter on a GET. Query strings are written to web-server logs, reverse-proxy and load-balancer logs, browser history and Referer headers, none of which are places a password may be stored - and the password is not hashed or encoded in transit beyond the TLS connection itself. Never call it from a browser or a mobile app directly; call it from your own server, over HTTPS only, and make sure your own logging does not record the URL. It is documented here because integrations already depend on it, not because it is a good way to sign a client in. If you are building something new, sign clients in against your own identity provider and use the client id from GET /api/v2/clients/emailexists to link the account.
The client is resolved by clientId AND email together - both must match the same Active client, and that client must have an e-mail address recorded. This verifies a password for a client you have already identified; it does not look a client up. Get the id from GET /api/v2/clients/emailexists first.
Success is 200 with an empty body. Failure is 401 with an empty body, and every kind of failure looks the same - wrong password, wrong e-mail for that id, unknown id, or an inactive client. That is deliberate; do not try to tell them apart.
This 401 is the one place in the API where 401 means something other than a credential problem. Your own bearer token is still fine - an authorization failure on this endpoint would have been a 403. Do not let a shared HTTP handler treat this 401 as "my token expired" and re-authenticate.
Leading and trailing whitespace is stripped from both the e-mail and the password before checking.
Passwords are held as PBKDF2 hashes. A client still on the older hash is migrated to PBKDF2 the first time they sign in successfully, so the first successful call for such a client also writes to their record.
Parameters
Parameters for GET /api/v2/clients/authenticate
Name
Type
In
Required
Description
clientId
Guid
query
Required
The client whose password is being checked.
email
String
query
Required
That same client's e-mail address. Must match the record.
password
String
query
Required
The password to check, in plain text. See the warning above.
Responses
Responses for GET /api/v2/clients/authenticate
Status
Meaning
200
Empty body. The password is correct for that client.
401
Empty body. The credentials are not valid - which part was wrong is not reported. This is not a problem with your bearer token.
403
Authorization failed - your API credential, not the client's password. See Errors.
Example
# Server-side only. Never put this URL anywhere that logs it.
curl -s -o /dev/null -w "%{http_code}" -X GET \
"https://acme.chidesk.com/api/v2/clients/authenticate?clientId=7e8f9012-3a4b-4c5d-6e7f-8091a2b3c4d5&email=jo%40example.com&password=$CLIENT_PASSWORD" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
# 200 = valid, 401 = not valid
// Both values must be URL-encoded, and neither should be logged.
var url = "api/v2/clients/authenticate"
+ "?clientId=" + clientId
+ "&email=" + Uri.EscapeDataString(email)
+ "&password=" + Uri.EscapeDataString(password);
var response = await http.GetAsync(url);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
// Wrong password, wrong e-mail for this id, or an inactive client.
// This does NOT mean the bearer token is stale - that would be a 403.
return false;
}
response.EnsureSuccessStatusCode();
return true;
Look up a client by mobile number, the same way emailexists looks one up by address.
The body is the client's id on its own, or the literal null.
The number is compared as text after trimming, so it has to be stored in the same format you send. There is no normalisation of country codes, spaces or leading zeros - +27821234567 does not match 0821234567.
Only Active clients with a mobile number recorded are considered.
Parameters
Parameters for GET /api/v2/clients/mobileexists
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site to search, plus the account-wide clients.
mobile
String
query
Required
The mobile number to look for, in the same format it is stored in.
Responses
Responses for GET /api/v2/clients/mobileexists
Status
Meaning
200
The client's id as a bare JSON string, or null.
403
Authorization failed. See Errors.
Example
curl -X GET \
"https://acme.chidesk.com/api/v2/clients/mobileexists?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&mobile=%2B27821234567" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
var url = "api/v2/clients/mobileexists"
+ "?siteId=" + siteId
+ "&mobile=" + Uri.EscapeDataString(mobile);
var response = await http.GetAsync(url);
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Guid? existingClientId = JsonConvert.DeserializeObject<Guid?>(body);
Build a marketing list: the clients who have agreed to be contacted, filtered by the channels they accepted.
This is not the client list with extra columns. Every client who has not agreed to receive marketing messages is excluded, always, whatever you pass - there is no parameter that turns that off. Use it as your marketing list and you inherit the account's consent record; use GET /api/v2/clients if you want every client.
The three channel filters are three-state. Leave one out to ignore that channel; send true for clients who accepted it; send false for clients who explicitly declined it. false is not the same as leaving it out.
find here is a contains match, not the starts-with match used by GET /api/v2/clients. The same term can return more rows here than there.
Ordered by last name.
Parameters
Parameters for GET /api/v2/clients/contacts
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose clients to read, plus the account-wide clients.
itemState
ItemStates
query
Optional
Active (1, the default) or Inactive (2).
find
String
query
Optional
Contains filter over name, code, e-mail, mobile and membership number.
contactEmail
Boolean
query
Optional
true for clients who accepted e-mail, false for clients who declined it. Omit to ignore the channel.
contactSms
Boolean
query
Optional
true for clients who accepted SMS, false for clients who declined it. Omit to ignore the channel.
contactPhone
Boolean
query
Optional
true for clients who accepted phone calls, false for clients who declined them. Omit to ignore the channel.
Responses
Responses for GET /api/v2/clients/contacts
Status
Meaning
200
An array of ClientContact objects - marketing opt-ins only.
See the fields
403
Authorization failed. See Errors.
Example
# Everyone at this site who agreed to marketing and accepted e-mail.
curl -X GET \
"https://acme.chidesk.com/api/v2/clients/contacts?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&contactEmail=true" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
var url = "api/v2/clients/contacts"
+ "?siteId=" + siteId
+ "&contactEmail=true";
var response = await http.GetAsync(url);
response.EnsureSuccessStatusCode();
// Already filtered to marketing opt-ins - do not re-broaden this list.
var json = await response.Content.ReadAsStringAsync();
List one client's past and future appointments at a site, with the invoice each one was billed on.
The range is capped at 730 days - two years, not the 90 days most date-ranged endpoints allow. A wider range returns 400 with "Maximum date range is 730 days".
An appointment is returned only when it falls entirely inside the range: it must start at or after start and end at or before end. One that straddles either edge is left out, so widen the range slightly rather than aligning it exactly to a month boundary.
Scoped to the site as well as the client, so a client seen at two sites needs one call per site.
Parameters
Parameters for GET /api/v2/clients/appointmenthistory
Name
Type
In
Required
Description
clientId
Guid
query
Required
The client whose history to read.
siteId
Guid
query
Required
The site to read it at.
start
DateTime
query
Required
Start of the range, UTC.
end
DateTime
query
Required
End of the range, UTC. At most 730 days after start.
Responses
Responses for GET /api/v2/clients/appointmenthistory
siteId travels in the query string; the client is the JSON body. Use an empty Guid for ClientId.
To give the client a portal password, send it in AccountPassword. ChiDesk hashes it and stores only the hash - the plain value is never written to the client record. Leave the field out and the client simply has no password.
On update the field is called NewAccountPassword instead. Sending AccountPassword to the PUT does not change the password.
Check for an existing client with GET /api/v2/clients/emailexists or /mobileexists first. Nothing here stops you creating a second record for the same person.
The id goes in the query string as clientId, not in the path - see Where the record id goes. There is no PUT /api/v2/clients/{id}.
The record that is loaded and saved is the one named by the query's clientId. The body's ClientId is not what selects it.
To change the password, send the new value in NewAccountPassword - not AccountPassword, which is the create-time field: a value sent there on a PUT is written to the record as sent and does not change the password. Leave NewAccountPassword out to keep the existing password.
Fetch the client first and send it back with your changes applied. Fields you send overwrite the stored ones.
// NewAccountPassword on update; AccountPassword only works on create.
var content = new StringContent(
JsonConvert.SerializeObject(client), Encoding.UTF8, "application/json");
var response = await http.PutAsync(
"api/v2/clients?clientId=" + clientId, content);
response.EnsureSuccessStatusCode();
Client custom forms
Read the answers clients have given on the account's custom forms - consultation cards, intake questionnaires, consent forms. V1 published this as Custom Forms.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
List completed form submissions captured in a date range, with the answers on each.
The range is matched against when the form was captured, not against the date of any appointment it belongs to.
The end of the range is widened by one day before the query runs, so a form captured during the end date itself is included. Passing the same value for start and end returns that whole day rather than nothing.
There is no cap on the range here, and no filter beyond the dates - a wide range on a busy account returns a lot of data. Page it yourself by asking for shorter periods.
All three parameters are required by name: none of them carries a default.
Parameters
Parameters for GET /api/v2/clientcustomforms
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose submissions to read.
start
DateTime
query
Required
Start of the capture-date range, UTC.
end
DateTime
query
Required
End of the capture-date range, UTC. Inclusive of the whole day.
Responses
Responses for GET /api/v2/clientcustomforms
Status
Meaning
200
An array of ClientCustomForm objects, each carrying its answers.
See the fields
403
Authorization failed. See Errors.
Example
curl -X GET \
"https://acme.chidesk.com/api/v2/clientcustomforms?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&start=2026-07-01%2000:00&end=2026-07-31%2000:00" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
var url = "api/v2/clientcustomforms"
+ "?siteId=" + siteId
+ "&start=2026-07-01 00:00"
+ "&end=2026-07-31 00:00";
var response = await http.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
Employees
List the staff at a site and read their rosters - shifts, leave and other time off. Employee ids are what you pass when you want an appointment with a particular person.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
Only siteId is required; itemState and find carry defaults and may be omitted.
This is every employee, not only the ones who can be booked. Whether a given employee can perform a given service is decided when you search for available times.
Read the roster for a site over a date range: who is on shift, and who is away.
The range is capped at 90 days. A wider range returns 400 with "Maximum date range is 90 days".
A schedule is returned when it overlaps the range at all, not only when it sits inside it - so a shift that started before start and ends inside it is included, and its own Start is before the range you asked for. Clip the times yourself if you are drawing a calendar.
This returns both working time and time off. Read the schedule type on each row rather than assuming a row means the employee is available.
includeClasses carries a default and may be omitted. Leave it out and class time is not part of the roster.
Parameters
Parameters for GET /api/v2/employees/schedules
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose roster to read.
start
DateTime
query
Required
Start of the range, UTC.
end
DateTime
query
Required
End of the range, UTC. At most 90 days after start.
includeClasses
Boolean
query
Optional
Set true to include the time employees are committed to classes. Defaults to false.
curl -X GET \
"https://acme.chidesk.com/api/v2/employees/schedules?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&start=2026-08-01%2000:00&end=2026-08-31%2000:00&includeClasses=true" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
var url = "api/v2/employees/schedules"
+ "?siteId=" + siteId
+ "&start=2026-08-01 00:00"
+ "&end=2026-08-31 00:00"
+ "&includeClasses=true";
var response = await http.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
Sites
List the branches on the account and read one in detail. Start here: almost every other endpoint needs a siteId, and this is the only way to discover them.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
curl -X GET \
"https://acme.chidesk.com/api/v2/sites/6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
var response = await http.GetAsync("api/v2/sites/" + siteId);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
Time records
Read clock-in and clock-out records for staff, for payroll and attendance. This is what was actually worked, as distinct from the roster in Employees.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
List clock-in and clock-out records in a date range.
The range is capped at 90 days. A wider range returns 400 with "Maximum date range is 90 days".
A record is returned when either its clock-in or its clock-out falls in the range, so a shift that ran over midnight at the edge of the range appears once, from the side that fell inside it.
The end of the range is widened by one day before the query runs, so records on the end date itself are included.
find is a contains match on the employee's first or last name.
find carries a default and may be omitted; siteId, start and end may not.
Records not tied to a single site are included whatever siteId you pass.
Ordered by clock-in time.
Parameters
Parameters for GET /api/v2/timerecords
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose records to read.
start
DateTime
query
Required
Start of the range, UTC.
end
DateTime
query
Required
End of the range, UTC. At most 90 days after start.
find
String
query
Optional
Contains filter on the employee's first or last name.
curl -X GET \
"https://acme.chidesk.com/api/v2/timerecords?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&start=2026-07-01%2000:00&end=2026-07-31%2000:00" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
var url = "api/v2/timerecords"
+ "?siteId=" + siteId
+ "&start=2026-07-01 00:00"
+ "&end=2026-07-31 00:00";
var response = await http.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
Services
The treatments a site sells, with their duration and price. Service ids are what you pass to the appointment search and to the appointment itself, so this is usually the second call an integration makes after listing sites.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
Only siteId is required. itemState, find and showPackageServices all carry defaults and may be left out.
Services that are components of a package - the individual treatments inside it, which are not sold on their own - are left out by default. Pass showPackageServices=true to see them as well.
A service with no site restriction on it belongs to the whole account and is returned for every siteId.
find is a contains match.
itemState is not a wildcard - see Conventions.
Parameters
Parameters for GET /api/v2/services
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose services to read.
itemState
ItemStates
query
Optional
Active (1, the default) or Inactive (2).
find
String
query
Optional
Contains filter over the service's name, code and category.
showPackageServices
Boolean
query
Optional
Set true to include services that only exist inside a package. Defaults to false.
Responses
Responses for GET /api/v2/services
Status
Meaning
200
An array of ServiceList objects, ordered by name.
See the fields
403
Authorization failed. See Errors.
Example
curl -X GET \
"https://acme.chidesk.com/api/v2/services?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&itemState=Active" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
var response = await http.GetAsync(
"api/v2/services?siteId=" + siteId + "&itemState=Active");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
Products
The retail stock a site carries, with cost, price and quantity on hand.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
Read the allowSale note carefully - the default is not "no filter". Leaving it out returns only the products that are NOT flagged for sale, which for most accounts is a short and surprising list. If you want the sellable retail range, pass allowSale=true explicitly. There is no way to ask for both in one call; make two.
Only siteId is required. itemState, find and allowSale all carry defaults and may be left out.
A product with no site restriction on it belongs to the whole account and is returned for every siteId.
Cost and quantity are read per site, so the same product returns different figures at different sites.
itemState is not a wildcard - see Conventions.
Parameters
Parameters for GET /api/v2/products
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose stock to read.
itemState
ItemStates
query
Optional
Active (1, the default) or Inactive (2).
find
String
query
Optional
Filter over the product's name, code and barcode.
allowSale
Boolean
query
Optional
true for products flagged for sale, false for products not flagged for sale. Defaults to false, which does filter - it is not "either".
# The sellable retail range. Without allowSale=true you get the opposite set.
curl -X GET \
"https://acme.chidesk.com/api/v2/products?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&itemState=Active&allowSale=true" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
var url = "api/v2/products"
+ "?siteId=" + siteId
+ "&itemState=Active"
+ "&allowSale=true"; // omit this and you get the NOT-for-sale products
var response = await http.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
Packages
Bundles of services sold as one item - a spa day, a course of treatments. A package id can be booked in place of a list of services when searching for available times.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
All three parameters are required by name here, including find. This is the one catalogue endpoint that declares no defaults, so unlike Services, Products, Courses and Memberships you cannot call it with siteId alone - that returns 404, not the list. Send itemState and find even when you do not want to filter: ...&itemState=Active&find=.
A package with no site restriction on it belongs to the whole account and is returned for every siteId.
find is a contains match over name, code and barcode.
itemState is not a wildcard - see Conventions.
Parameters
Parameters for GET /api/v2/packages
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose packages to read.
itemState
ItemStates
query
Required
Active (1) or Inactive (2). Required - there is no default.
find
String
query
Required
Contains filter over name, code and barcode. Required by name; send it empty for no filter.
Responses
Responses for GET /api/v2/packages
Status
Meaning
200
An array of PackageList objects, ordered by name.
See the fields
403
Authorization failed. See Errors.
404
A parameter was missing from the URL, so no endpoint matched. All three are required by name.
Example
# All three names present. find is empty but must be there.
curl -X GET \
"https://acme.chidesk.com/api/v2/packages?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&itemState=Active&find=" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
// Unlike /services and /products, none of these may be dropped.
var url = "api/v2/packages"
+ "?siteId=" + siteId
+ "&itemState=Active"
+ "&find=";
var response = await http.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
Courses
Pre-paid blocks of sessions a client buys up front and draws down over time - ten massages, six laser treatments.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
curl -X GET \
"https://acme.chidesk.com/api/v2/courses?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&itemState=Active" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
var response = await http.GetAsync(
"api/v2/courses?siteId=" + siteId + "&itemState=Active");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
Classes
Group sessions and events: the class definitions, the scheduled instances of them, and the bookings clients hold on those instances.
Three resources live under this one stem, and each needs its own allow-list line. A credential allowed only api/v2/classes cannot read schedules or bookings.
The three are layered: a class is the definition, a class schedule is one dated instance of it, and a class booking is one client's place on one schedule. Work down that order - a booking needs a ClassScheduleId, which comes from the schedules list.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
Both parameters are required by name; neither carries a default.
start is not a range and not a filter on when the class runs - it drops classes that had already finished by that moment. Pass the current time to get the classes still on offer, or an earlier date to include ones that have since ended.
Only Active classes are returned. There is no itemState here.
Parameters
Parameters for GET /api/v2/classes
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose classes to read.
start
DateTime
query
Required
Exclude classes that had already ended before this moment, UTC.
Responses
Responses for GET /api/v2/classes
Status
Meaning
200
An array of ClassList objects, ordered by name.
See the fields
403
Authorization failed. See Errors.
Example
curl -X GET \
"https://acme.chidesk.com/api/v2/classes?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&start=2026-08-01%2000:00" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
var url = "api/v2/classes"
+ "?siteId=" + siteId
+ "&start=" + DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm");
var response = await http.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
List the scheduled instances of classes in a date range - the timetable a client would book from.
bookOnline defaults to true, and that default filters: leave it out and you only see schedules whose class is flagged as bookable online. Pass bookOnline=false to see the whole timetable including the internal-only sessions.
Only Active schedules are returned; cancelled and inactive ones are not, and there is no parameter to include them.
classType picks Class (1) or Event (2). The value All (0) is not a wildcard - it matches a class type of 0, which nothing has. Make one call per type if you need both.
siteId, classType, start and end are required by name. find, employeeId and bookOnline carry defaults and may be omitted.
There is no cap on the date range here.
Parameters
Parameters for GET /api/v2/classes/classschedules
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose timetable to read.
classType
ClassTypes
query
Required
Class (1) or Event (2). Not a wildcard - All (0) returns nothing.
start
DateTime
query
Required
Start of the range, UTC.
end
DateTime
query
Required
End of the range, UTC.
find
String
query
Optional
Name filter. Omit for no filter.
employeeId
Guid
query
Optional
Only the schedules this employee is taking. Omit for all employees.
bookOnline
Boolean
query
Optional
Defaults to true, which returns only classes flagged bookable online. Pass false for the whole timetable.
# The public timetable for August.
curl -X GET \
"https://acme.chidesk.com/api/v2/classes/classschedules?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&classType=Class&start=2026-08-01%2000:00&end=2026-08-31%2000:00" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
# Everything, including sessions not published for online booking.
curl -X GET \
"https://acme.chidesk.com/api/v2/classes/classschedules?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&classType=Class&start=2026-08-01%2000:00&end=2026-08-31%2000:00&bookOnline=false" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
var url = "api/v2/classes/classschedules"
+ "?siteId=" + siteId
+ "&classType=Class"
+ "&start=2026-08-01 00:00"
+ "&end=2026-08-31 00:00"
+ "&bookOnline=false"; // omit to see only the online-bookable ones
var response = await http.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
The id goes in the query string as classId, not in the path - see Where the record id goes. There is no PUT /api/v2/classes/{id}.
The record loaded is the one named by the query's classId, but the site it is saved against comes from the body's SiteId. Always send SiteId in the body; leave it out and the class is saved against an empty site id, which is not recoverable through the API.
Fetch the class first and send it back with your changes applied.
// SiteId in the body is not optional - it is the site the record is saved against.
var content = new StringContent(
JsonConvert.SerializeObject(classModel), Encoding.UTF8, "application/json");
var response = await http.PutAsync(
"api/v2/classes?classId=" + classModel.ClassId, content);
response.EnsureSuccessStatusCode();
Two things about this endpoint will silently do nothing if you get them wrong, and both still return 200. First, you must set ObjectState to 1 (Created) in the body: a booking is only written when its ObjectState says it changed, and the default of 0 (Unchanged) means the request is accepted and discarded. Second, ChiDesk does not generate the booking id - you generate the Guid, send it as ClassBookingId, and it is echoed straight back to you. Sending an empty Guid gets you an empty Guid in the response. Always read back the schedule or the bookings list to confirm the booking landed; do not treat the 200 as proof.
siteId travels in the query string; the booking is the JSON body.
ClassScheduleId in the body says which dated session the client is booking - it is read from the body, not the query string, and the whole schedule is loaded and re-saved around your booking.
The response Value is whatever ClassBookingId you sent, not a server-assigned id.
Parameters
Parameters for POST /api/v2/classes/classbookings
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site the class runs at.
classBooking
ClassBooking
body
Required
The booking. Requires ClassBookingId (a Guid you generate), ClassScheduleId, the client, and ObjectState set to 1.
Responses
Responses for POST /api/v2/classes/classbookings
Status
Meaning
200
An object of the form { "Message": "ClassBookingId", "Value": "<guid>" }, echoing the id you sent. A 200 does not prove the booking was written - check ObjectState.
var booking = new
{
ClassBookingId = Guid.NewGuid(), // you generate this, not ChiDesk
ClassScheduleId = classScheduleId,
ClientId = clientId,
ObjectState = 1 // 1 = Created. Without this nothing is written.
};
var content = new StringContent(
JsonConvert.SerializeObject(booking), Encoding.UTF8, "application/json");
var response = await http.PostAsync(
"api/v2/classes/classbookings?siteId=" + siteId, content);
response.EnsureSuccessStatusCode();
// The response echoes the id you sent. Read the bookings list back to confirm.
Change an existing class booking - move it, or cancel a client's place.
The id in the URL is ignored
The classBookingId query parameter is required for the request to reach this endpoint at all, and is then never read. The booking that is actually loaded and saved is the one named by ClassBookingId in the body. Send the same value in both places: if they differ, you will change the booking named in the body while the URL and the response both name the other one. As on create, a booking whose ObjectState is left at 0 is accepted and not written - set 2 (Updated) to change one, or 3 (Deleted) to remove it.
No siteId is needed: the site is looked up from the booking itself.
The response Value is the body's ClassBookingId, not the query parameter's.
ClassScheduleId in the body says which session the booking belongs to after the change, so moving a client to another session means changing it.
Parameters
Parameters for PUT /api/v2/classes/classbookings
Name
Type
In
Required
Description
classBookingId
Guid
query
Required
Required for the endpoint to be matched, then ignored. Keep it equal to the body's ClassBookingId.
classBooking
ClassBooking
body
Required
The booking. ClassBookingId here is what selects the record. Set ObjectState to 2 to update or 3 to remove.
Responses
Responses for PUT /api/v2/classes/classbookings
Status
Meaning
200
An object of the form { "Message": "ClassBookingId", "Value": "<guid>" }, taken from the body. Not proof that anything changed - check ObjectState.
400
No body was sent, or the body failed validation.
403
Authorization failed. See Errors.
Example
# Cancel a client's place. Note the id appears twice, identically.
curl -X PUT \
"https://acme.chidesk.com/api/v2/classes/classbookings?classBookingId=e4f50617-2839-4a5b-8c6d-7e8f90a1b2c3" \
-H "Authorization: Bearer $CHIDESK_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"ClassBookingId": "e4f50617-2839-4a5b-8c6d-7e8f90a1b2c3",
"ClassScheduleId": "b2c3d4e5-f607-4819-a2b3-c4d5e6f70819",
"ObjectState": 3
}'
// The body's ClassBookingId is what gets acted on - keep the query value equal to it.
var content = new StringContent(
JsonConvert.SerializeObject(booking), Encoding.UTF8, "application/json");
var response = await http.PutAsync(
"api/v2/classes/classbookings?classBookingId=" + booking.ClassBookingId, content);
response.EnsureSuccessStatusCode();
Memberships
The membership plans a site sells - the recurring schemes that give clients preferential rates or included sessions.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
Only siteId is required; itemState and find carry defaults and may be omitted.
These are the plans on offer, not the clients who hold one. A client's own membership is on their Client record.
A plan with no site restriction on it belongs to the whole account and is returned for every siteId.
find is a contains match over name and code.
itemState is not a wildcard - see Conventions.
Parameters
Parameters for GET /api/v2/memberships
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose membership plans to read.
itemState
ItemStates
query
Optional
Active (1, the default) or Inactive (2).
find
String
query
Optional
Contains filter over name and code.
Responses
Responses for GET /api/v2/memberships
Status
Meaning
200
An array of MembershipList objects, ordered by name.
See the fields
403
Authorization failed. See Errors.
Example
curl -X GET \
"https://acme.chidesk.com/api/v2/memberships?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&itemState=Active" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
var response = await http.GetAsync(
"api/v2/memberships?siteId=" + siteId + "&itemState=Active");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
Sales
Invoices, credit notes, quotations and the purchasing side - orders, supplier invoices, goods received and stock transfers. Every one of these is an accounting document, and they all live under /api/v2/accountingdocuments. V1 published this category as Sales.
Money moving is two separate records in ChiDesk: the document that says what was owed (here), and the entry that says what was paid (Receipts). A paid invoice is an accounting document with an accounting entry against it - reconcile across both categories, not one.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
List accounting documents in a date range, either the client-facing ones or the supplier-facing ones.
All five parameters are required by name, including find. Send it empty when you do not want to filter: ...&find=. Leaving a name out does not return the list - verified against a running server, it returns 405, because POST /api/v2/accountingdocuments shares this path and keeps it alive for another method once the GET is ruled out.
clientDocuments is a switch between two fixed sets, not a filter you can widen. true returns client invoices, credit notes and quotations. false returns debit notes, goods received notes, purchase orders, supplier invoices and stock transfers. There is no value that returns both, so a full picture is two calls.
Neither side returns the two recurring document types - recurring client invoices and recurring supplier invoices are absent from this list whatever you pass, with no error. If you reconcile invoices from this endpoint alone you will be short by exactly the recurring ones. Subscribe to the NewRecurringClientInvoice and NewRecurringSupplierInvoice WebHook events to catch them.
The end of the range is widened by one day before the query runs, so documents dated on the end date itself are included - and so are any dated in the first moments of the following day. Do not use this endpoint to build non-overlapping day buckets without allowing for that.
find is a starts-with match on the client or supplier name, or on the document number.
There is no cap on the date range here, unlike the appointment and roster endpoints.
Ordered by document date. Each row also carries a fiscal status where the account is fiscalised.
Parameters
Parameters for GET /api/v2/accountingdocuments
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose documents to read.
clientDocuments
Boolean
query
Required
true for invoices, credit notes and quotations; false for the purchasing documents.
start
DateTime
query
Required
Start of the document-date range, UTC.
end
DateTime
query
Required
End of the document-date range, UTC. Widened by a day internally.
find
String
query
Required
Starts-with filter on client or supplier name, or document number. Required by name; send it empty for no filter.
Responses
Responses for GET /api/v2/accountingdocuments
Status
Meaning
200
An array of AccountingDocumentList objects, ordered by document date.
See the fields
403
Authorization failed. See Errors.
405
A parameter was missing from the URL, so this action did not match. The path survives for POST, so the failure surfaces as 405 rather than 404.
Example
# Client-facing documents for July.
curl -X GET \
"https://acme.chidesk.com/api/v2/accountingdocuments?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&clientDocuments=true&start=2026-07-01%2000:00&end=2026-07-31%2000:00&find=" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
# The purchasing side is the same call with clientDocuments=false.
curl -X GET \
"https://acme.chidesk.com/api/v2/accountingdocuments?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&clientDocuments=false&start=2026-07-01%2000:00&end=2026-07-31%2000:00&find=" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
// Two calls, because clientDocuments has no "both" value.
async Task<string> Documents(bool clientDocuments)
{
var url = "api/v2/accountingdocuments"
+ "?siteId=" + siteId
+ "&clientDocuments=" + (clientDocuments ? "true" : "false")
+ "&start=2026-07-01 00:00"
+ "&end=2026-07-31 00:00"
+ "&find=";
var response = await http.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
var clientSide = await Documents(true);
var supplierSide = await Documents(false);
// Recurring invoices are in neither - pick those up from the WebHook events.
Fetch one accounting document in full, including its line items.
This route takes no siteId, and no clientDocuments switch - any document type can be fetched by its id, including the recurring ones the list will not show you.
The id must be a well-formed Guid. Anything else does not match the route and returns 404.
Parameters
Parameters for GET /api/v2/accountingdocuments/{accountingDocumentId}
Name
Type
In
Required
Description
accountingDocumentId
Guid
route
Required
The document to fetch.
Responses
Responses for GET /api/v2/accountingdocuments/{accountingDocumentId}
Create an accounting document - raise an invoice, a credit note, a quotation or a purchase order.
AccountingDocumentTypeId in the body decides what kind of document this is, and there is no default: 1 client invoice, 2 quotation, 3 credit note, 9 purchase order, 10 supplier invoice, 11 debit note, 12 goods received note, 13 stock transfer. Get it wrong and you get a valid document of the wrong type, not an error.
Send SiteId in the body as well as siteId in the query string, and make them the same site. The new document is set up from the body's SiteId and then saved against the query's - they are read from different places, so a mismatch is not something the API will tell you about.
Use an empty Guid for AccountingDocumentId. LinkedAccountingDocumentId ties a credit note back to the invoice it credits.
Creating a document does not record a payment against it. That is a separate accounting entry - see Receipts.
Parameters
Parameters for POST /api/v2/accountingdocuments
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site to create the document at. Keep it equal to the body's SiteId.
// AccountingDocumentTypeId 1 = client invoice. The body's SiteId must match
// the siteId in the query string.
var content = new StringContent(
JsonConvert.SerializeObject(document), Encoding.UTF8, "application/json");
var response = await http.PostAsync(
"api/v2/accountingdocuments?siteId=" + siteId, content);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
// { "Message": "AccountingDocumentId", "Value": "..." }
The document that is loaded is the one named in the route - unlike the appointments PUT, which loads the one named in the body.
The site it is saved against comes from the body's SiteId. Send it, and send the document's real site; leave it out and the document is saved against an empty site id.
Fetch the document first and send it back with your changes applied. Fields you send overwrite the stored ones.
A document that has already been posted or fiscalised should not be edited through here - correct it with a credit note instead.
Parameters
Parameters for PUT /api/v2/accountingdocuments/{accountingDocumentId}
Name
Type
In
Required
Description
accountingDocumentId
Guid
route
Required
The document to update. This is what selects the record.
A document that does not exist comes back as 400 with the body "Record does not exist." - not 404. A 404 from this path means the id was not a well-formed Guid.
This removes the document rather than reversing it. On a posted or fiscalised document that is not what your accountant wants - raise a credit note instead.
Parameters
Parameters for DELETE /api/v2/accountingdocuments/{accountingDocumentId}
Name
Type
In
Required
Description
accountingDocumentId
Guid
route
Required
The document to remove.
Responses
Responses for DELETE /api/v2/accountingdocuments/{accountingDocumentId}
Status
Meaning
200
The document was removed.
400
No document exists with that id - the body reads "Record does not exist."
var response = await http.DeleteAsync(
"api/v2/accountingdocuments/" + accountingDocumentId);
// A missing document is a 400, not a 404.
response.EnsureSuccessStatusCode();
Receipts
Money in and money out: the receipts taken from clients and the payments made to suppliers, including the recurring ones. These are accounting entries and live under /api/v2/accountingentries. V1 published this category as Receipts.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
List accounting entries of one type in a date range.
All five parameters are required by name, including find. Send it empty when you do not want to filter. Leaving a name out returns 405 - verified against a running server - because POST /api/v2/accountingentries shares this path and keeps it alive for another method once the GET is ruled out.
entryType matches exactly one type per call: Receipt (1), Payment (2), RecurringReceipt (10) or RecurringPayment (11). There is no "all" value, so a full day's cash movement is up to four calls. Both names and numbers are accepted.
Unlike the Sales list, the recurring types are reachable here - ask for them by name.
The end of the range is widened by one day before the query runs, so entries dated on the end date itself are included, along with any in the first moments of the following day.
There is no cap on the date range.
Parameters
Parameters for GET /api/v2/accountingentries
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose entries to read.
entryType
AccountingEntryTypes
query
Required
Exactly one of Receipt (1), Payment (2), RecurringReceipt (10) or RecurringPayment (11).
start
DateTime
query
Required
Start of the entry-date range, UTC.
end
DateTime
query
Required
End of the entry-date range, UTC. Widened by a day internally.
find
String
query
Required
Filter on the client or supplier name, or the entry number. Required by name; send it empty for no filter.
A parameter was missing from the URL, so this action did not match. The path survives for POST, so the failure surfaces as 405 rather than 404.
Example
# Receipts for July. Payments are the same call with entryType=Payment.
curl -X GET \
"https://acme.chidesk.com/api/v2/accountingentries?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70&entryType=Receipt&start=2026-07-01%2000:00&end=2026-07-31%2000:00&find=" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
// One call per type - entryType has no wildcard.
var types = new[] { "Receipt", "Payment", "RecurringReceipt", "RecurringPayment" };
foreach (var entryType in types)
{
var url = "api/v2/accountingentries"
+ "?siteId=" + siteId
+ "&entryType=" + entryType
+ "&start=2026-07-01 00:00"
+ "&end=2026-07-31 00:00"
+ "&find=";
var response = await http.GetAsync(url);
response.EnsureSuccessStatusCode();
}
AccountingEntryTypeId in the body decides what this entry is: 1 receipt, 2 payment, 10 recurring receipt, 11 recurring payment. There is no default.
Send SiteId in the body as well as siteId in the query string, and make them the same site - the entry is set up from the body's SiteId and then saved against the query's.
Use an empty Guid for AccountingEntryId.
To mark an invoice as paid, create the entry here and allocate it against the document; the document itself is not edited.
Parameters
Parameters for POST /api/v2/accountingentries
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site to record the entry at. Keep it equal to the body's SiteId.
var response = await http.DeleteAsync(
"api/v2/accountingentries/" + accountingEntryId);
response.EnsureSuccessStatusCode();
Vouchers
Gift vouchers: list and search them, read one, issue a new one, and update an existing one. Voucher numbers have to be unique across the site.
Both write endpoints take the site from the body's SiteId, not only from the query string. Always send SiteId in the body - on the update it is dereferenced without a null check, so omitting it fails with a 500 rather than a helpful 400.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
Only siteId is required; itemState and find carry defaults and may be omitted.
find is a starts-with match over the voucher number, the recipient and the barcode. To look a voucher up from a barcode scan, send the scanned value as find.
Vouchers not tied to a single site are included whatever siteId you pass.
itemState is not a wildcard - see Conventions.
Parameters
Parameters for GET /api/v2/vouchers
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose vouchers to read.
itemState
ItemStates
query
Optional
Active (1, the default) or Inactive (2).
find
String
query
Optional
Starts-with filter over voucher number, recipient and barcode.
VoucherNumber has to be unique. If it is already in use the call returns 400 with a model-state error on VoucherNumber reading "A voucher with this number already exists." - the message is in the account's language, so match on the field name rather than the text.
The uniqueness check runs against the body's SiteId, while the voucher is created at the query string's siteId. Send the same site in both, or you can be told a number is free at one site and have the voucher created at another.
A blank or missing VoucherNumber skips the uniqueness check altogether - it does not fail validation here.
Use an empty Guid for VoucherId. Send SiteId in the body.
Parameters
Parameters for POST /api/v2/vouchers
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site to issue the voucher at. Keep it equal to the body's SiteId.
var content = new StringContent(
JsonConvert.SerializeObject(voucher), Encoding.UTF8, "application/json");
var response = await http.PostAsync(
"api/v2/vouchers?siteId=" + siteId, content);
if (response.StatusCode == HttpStatusCode.BadRequest)
{
// Check the model state for VoucherNumber - the number may already exist.
var errors = await response.Content.ReadAsStringAsync();
}
The voucher that is loaded is the one named in the route.
SiteId in the body is mandatory in practice even though nothing says so: it is used to save the record and is read without a null check, so a body without it fails with a 500. Send the voucher's real site.
The uniqueness rule is not applied on update - changing VoucherNumber to one that already exists is not rejected here. Check it yourself with the list endpoint first.
Fetch the voucher first and send it back with your changes applied.
Parameters
Parameters for PUT /api/v2/vouchers/{voucherId}
Name
Type
In
Required
Description
voucherId
Guid
route
Required
The voucher to update. This is what selects the record.
// SiteId must be present in the body or the request fails with a 500.
var content = new StringContent(
JsonConvert.SerializeObject(voucher), Encoding.UTF8, "application/json");
var response = await http.PutAsync(
"api/v2/vouchers/" + voucherId, content);
response.EnsureSuccessStatusCode();
Reports
The sales report: takings over a period, broken down by employee and including voucher redemptions. One endpoint, and the only one that can span the whole account in a single call.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
Run the sales report over a date range, for one site or for the whole account.
The range is capped at 365 days here, not the 90 days that appointments and rosters allow. A wider range returns 400 with "Maximum date range is 365 days".
siteId is genuinely optional and may be left out entirely. Leave it out and the report covers every Active site on the account - inactive sites are not included even though their history exists. Supply it and the report is scoped to that one site, active or not.
Employee names and voucher redemptions are always included; there is no parameter to turn them off.
This is the most expensive endpoint in the API. It reads a year of transactions across every site if you let it, and the rate limit still applies - ask for one wide range rather than looping over weeks.
Parameters
Parameters for GET /api/v2/reports/sales
Name
Type
In
Required
Description
start
DateTime
query
Required
Start of the range, UTC.
end
DateTime
query
Required
End of the range, UTC. At most 365 days after start.
siteId
Guid
query
Optional
One site to report on. Omit for every Active site on the account.
Responses
Responses for GET /api/v2/reports/sales
Status
Meaning
200
An array of Sales objects, one per site included in the report.
See the fields
400
The range exceeds 365 days.
403
Authorization failed. See Errors.
Example
# Whole account for July - no siteId at all.
curl -X GET \
"https://acme.chidesk.com/api/v2/reports/sales?start=2026-07-01%2000:00&end=2026-07-31%2000:00" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
# One site.
curl -X GET \
"https://acme.chidesk.com/api/v2/reports/sales?start=2026-07-01%2000:00&end=2026-07-31%2000:00&siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
// Omitting siteId is what makes this account-wide - it is not a required blank.
var url = "api/v2/reports/sales"
+ "?start=2026-07-01 00:00"
+ "&end=2026-07-31 00:00";
var response = await http.GetAsync(url);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
WebHooks
Register a URL and ChiDesk will POST to it when the event you asked for happens, so you do not have to poll. The polling endpoint is here to help you build and test your handler before you go live.
The two allow-list keys are independent. A credential allowed only api/v2/webhooks/poll cannot register or unregister hooks, and one allowed only api/v2/webhooks cannot poll.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
Register a URL to be called when an event happens.
The two body fields are snake_case - target_url and event - unlike the PascalCase fields everywhere else in the API. They are read straight off the raw JSON body.
Both fields are required, and neither is validated. If either is missing the request fails with a 500 rather than a 400 with a helpful message.
A URL may be registered once. Registering a target_url that already exists returns 409 Conflict and changes nothing - to point an existing hook at a different event, unregister it first.
ChiDesk records the client_id of the credential that registered the hook, so you can see which integration owns it.
Parameters
Parameters for POST /api/v2/webhooks
Name
Type
In
Required
Description
target_url
String
body
Required
The absolute HTTPS URL ChiDesk should POST to.
event
String
body
Required
Which event to subscribe to. Use one of the event names listed below.
Responses
Responses for POST /api/v2/webhooks
Status
Meaning
201
The hook was created. The body is { "id": <int> }.
var hook = new
{
target_url = "https://hooks.example.com/chidesk/appointments",
@event = "NewAppointment" // event is a C# keyword - the wire name is still "event"
};
var content = new StringContent(
JsonConvert.SerializeObject(hook), Encoding.UTF8, "application/json");
var response = await http.PostAsync("api/v2/webhooks", content);
if (response.StatusCode == HttpStatusCode.Conflict)
{
// This target_url is already registered.
}
This DELETE carries a request body, which is unusual - some HTTP clients will not send one on a DELETE without being told to. In .NET, build an HttpRequestMessage and set its Content rather than calling DeleteAsync.
The hook is identified by target_url, not by the id returned when it was created.
Unregistering a URL that was never registered still returns 200.
// DeleteAsync cannot carry a body - build the request by hand.
var request = new HttpRequestMessage(HttpMethod.Delete, "api/v2/webhooks")
{
Content = new StringContent(
"{ \"target_url\": \"https://hooks.example.com/chidesk/appointments\" }",
Encoding.UTF8,
"application/json")
};
var response = await http.SendAsync(request);
response.EnsureSuccessStatusCode();
Fetch the most recent records for an event, so you can build a handler against real payloads before registering a hook.
This endpoint is a development aid, not a substitute for registering a hook. It returns the latest records for the event rather than only ones you have not seen.
An event name that is not recognised returns 200 with an empty body rather than an error - so a typo looks like "no data". Check the name against the table below.
siteId and records are genuinely optional and may be omitted. When siteId is left out ChiDesk uses the account's main site.
Parameters
Parameters for GET /api/v2/webhooks/poll
Name
Type
In
Required
Description
targetEvent
String
query
Required
Which event's payload to return. One of the event names below.
siteId
Guid
query
Optional
The site to read from. Defaults to the account's main site.
records
Int32
query
Optional
How many recent records to return. Defaults to 1.
Responses
Responses for GET /api/v2/webhooks/poll
Status
Meaning
200
An array of the object type that belongs to the event - see the table below. Empty when the event name is not recognised.
403
Authorization failed. See Errors.
Example
curl -X GET \
"https://acme.chidesk.com/api/v2/webhooks/poll?targetEvent=NewAppointment&records=5" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
var response = await http.GetAsync(
"api/v2/webhooks/poll?targetEvent=NewAppointment&records=5");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
Event names
The name you register a hook with, and the name you pass to
targetEvent when polling. Each one delivers the object in the second
column. Names are case-sensitive, and an unrecognised name is not reported as an
error - it simply returns nothing.
WebHook event names and their payloads
Event
Payload
Fires when
NewAppointment
Appointment
An appointment was booked.
NewClient
Client
A client record was created.
NewClientInvoice
AccountingDocument
A client was invoiced.
NewCreditNote
AccountingDocument
A credit note was raised.
NewQuotation
AccountingDocument
A quotation was issued.
NewReceipt
AccountingEntry
Money was received.
NewPayment
AccountingEntry
A payment was made.
NewSupplierInvoice
AccountingDocument
A supplier invoice was captured.
NewPurchaseOrder
AccountingDocument
A purchase order was raised.
NewGoodsReceivedNote
AccountingDocument
Stock was received.
NewDebitNote
AccountingDocument
A debit note was raised.
NewStockTransfer
AccountingDocument
Stock moved between sites.
NewRecurringClientInvoice
AccountingDocument
A recurring client invoice was generated.
NewRecurringSupplierInvoice
AccountingDocument
A recurring supplier invoice was generated.
NewRecurringReceipt
AccountingEntry
A recurring receipt was generated.
NewRecurringPayment
AccountingEntry
A recurring payment was generated.
Alerts
The alert definitions configured on the account - the flags that can be raised against a client or an appointment. Read-only: alerts are set up in the back office, not through the API.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
siteId is the only parameter, and it is required by name.
Alerts not tied to a single site belong to the whole account and are returned for every siteId.
Ordered by category, then name - use the category to group them in your own UI.
Every row comes back with its selected flag false. That field is for the back-office editor; it does not tell you which alerts are set on a given client.
Parameters
Parameters for GET /api/v2/alerts
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose alerts to read.
Responses
Responses for GET /api/v2/alerts
Status
Meaning
200
An array of Alert objects, ordered by category then name.
See the fields
403
Authorization failed. See Errors.
Example
curl -X GET \
"https://acme.chidesk.com/api/v2/alerts?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
var response = await http.GetAsync("api/v2/alerts?siteId=" + siteId);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
ClassPass
ChiDesk's implementation of the ClassPass marketplace partner API. ClassPass calls these endpoints to publish a site's practitioners and availability, to book appointments on a member's behalf, and to reconcile attendance afterwards. If you are not ClassPass, you almost certainly want the Appointments category instead.
This category follows the ClassPass specification, not ChiDesk's own conventions. Field names are snake_case, ids are Guids with the hyphens stripped out, dates are ISO 8601, and times in the availability and appointment payloads are the site's LOCAL times without a zone designator - not UTC like the rest of this reference. Read the note on each endpoint.
A venue_id is a ChiDesk siteId with the hyphens removed, and a service_id or bookable_id is the corresponding ChiDesk id with the hyphens removed. Both forms are accepted on the way in.
The partner_id segment in every path is ignored - the account is resolved from the host name you called, the same as everywhere else in the API. Any value there returns your own account's data, and there is no way to reach another account's.
Leave the endpoint allow-list empty on a ClassPass credential. Almost every path here ends in an id, and paths ending in an id cannot be matched by the suffix test - see Allow-lists on a credential.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
List partners. Always exactly one - the account you authenticated against.
A ChiDesk account is one ClassPass partner, so this always returns a single partner and a pagination block that says page 1 of 1. page and page_size are accepted and have no effect on what comes back.
last_updated is generated at request time as "yesterday", not read from the record. Do not use it to detect changes.
This is the one path in the category that ends in a literal segment, so api/v2/classpass is the only usable endpoint allow-list line here.
Parameters
Parameters for GET /api/v2/classpass
Name
Type
In
Required
Description
page
Int32
query
Optional
Accepted for spec compliance. Defaults to 1 and has no effect.
page_size
Int32
query
Optional
Accepted for spec compliance. Defaults to 10 and is echoed back in the pagination block.
Responses
Responses for GET /api/v2/classpass
Status
Meaning
200
A partner_list object: one partner, plus a pagination block that always reads page 1 of 1.
See the fields
403
Authorization failed. See Errors.
Example
curl -X GET \
"https://acme.chidesk.com/api/v2/classpass" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
The partner_id in the path is not checked against anything. Whatever you put there, you get the account resolved from the host name - so this returns the same record as the list above, wrapped in a partner_item instead of an array.
last_updated is generated at request time, not stored.
Parameters
Parameters for GET /api/v2/classpass/{partner_id}
Name
Type
In
Required
Description
partner_id
String
route
Required
Required by the route, then ignored. The account comes from the host name.
List the account's venues - one per Active ChiDesk site.
Do not drive paging from the pagination block
The pagination block on this response is not the pagination of this response. The venues array is paged by the page and page_size you sent, but the pagination object beside it is overwritten just before the response is built and always reads page 1, page_size 10, total_pages 1. Drive a paging loop from it and you will stop after the first page and miss every venue after the tenth. Page until you get a short or empty venues array instead, or ask for a page_size larger than the number of sites and read it once.
Only Active sites are published. An inactive site is not a venue.
venue_id is the ChiDesk siteId with its hyphens removed.
The country in the address is translated from the site's stored country name to a two-letter code where ChiDesk recognises the name. A country it does not recognise is passed through as the stored text, so handle both.
phone is always null on this endpoint.
last_updated is generated at request time, not read from the site.
Parameters
Parameters for GET /api/v2/classpass/{partner_id}/venues
Name
Type
In
Required
Description
partner_id
String
route
Required
Required by the route, then ignored.
page
Int32
query
Optional
Which page of venues to return. Defaults to 1. The venues array honours this; the pagination block does not.
page_size
Int32
query
Optional
How many venues per page. Defaults to 10.
Responses
Responses for GET /api/v2/classpass/{partner_id}/venues
Status
Meaning
200
A venue_list object. The venues array is paged; the pagination block always reads page 1 of 1.
See the fields
403
Authorization failed. See Errors.
Example
# Ask for more than you have, so the broken pagination block cannot mislead you.
curl -X GET \
"https://acme.chidesk.com/api/v2/classpass/acme/venues?page=1&page_size=200" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
// Do NOT loop on pagination.total_pages here - it is hardcoded to 1.
var page = 1;
var pageSize = 50;
var venues = new List<JToken>();
while (true)
{
var url = "api/v2/classpass/" + partnerId + "/venues"
+ "?page=" + page + "&page_size=" + pageSize;
var response = await http.GetAsync(url);
response.EnsureSuccessStatusCode();
var body = JObject.Parse(await response.Content.ReadAsStringAsync());
var batch = body["venues"].ToList();
venues.AddRange(batch);
if (batch.Count < pageSize) break; // short page = last page
page++;
}
List the bookable services at a venue, with duration and cancellation window.
Only Active services are published, and services that exist only inside a package are left out.
duration and late_cancel_window are XML Schema durations - PT1H for an hour, PT30M for thirty minutes - not minutes and not ISO timestamps.
start_interval is the site's booking time scale in minutes: the granularity that start times are offered on.
late_cancel_window comes from the site's cancellation-period setting, so it is the same for every service at the venue.
Paging works correctly on this endpoint - the pagination block matches what you asked for. That is not true of the venues endpoint.
An allow-list line of api/v2/services does NOT reach this path, and a bare line of services would match both this and the ordinary service list. Leave the allow-list empty for ClassPass credentials.
Parameters
Parameters for GET /api/v2/classpass/{partner_id}/venues/{venue_id}/services
Name
Type
In
Required
Description
partner_id
String
route
Required
Required by the route, then ignored.
venue_id
String
route
Required
The ChiDesk siteId, hyphens optional.
page
Int32
query
Optional
Which page to return. Defaults to 1.
page_size
Int32
query
Optional
How many per page. Defaults to 10.
Responses
Responses for GET /api/v2/classpass/{partner_id}/venues/{venue_id}/services
Status
Meaning
200
A service_list object with a correct pagination block.
See the fields
403
Authorization failed. See Errors.
Example
curl -X GET \
"https://acme.chidesk.com/api/v2/classpass/acme/venues/6f1b8c023a4d4e5f9a012b3c4d5e6f70/services?page=1&page_size=50" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
List each practitioner's free windows at a venue over a date range.
start_date and end_date are required, and they are the venue's LOCAL dates, not UTC. The windows that come back are local times in ISO 8601 with no zone designator - so 2026-08-03T09:00:00 means nine in the morning at that venue.
end_date behaves as exclusive: the days walked are start_date up to but not including end_date, because the count is the whole-day difference between them. Ask for one day past the last day you want.
The result is cached for five minutes against the exact combination of partner, venue, start_date and end_date - which is what makes paging through a large range cheap, and also means a booking made in the last five minutes may still show as free. Change the range or wait it out if you need it fresh.
A practitioner with no free windows in the range is left out of the response entirely rather than returned with an empty array.
Windows are computed from shifts (or the site's trading hours, for full-time staff), minus existing appointments and time off, and closed days are skipped altogether.
The pagination block is correct here, but note that total_pages is computed before the page is sliced.
Parameters
Parameters for GET /api/v2/classpass/{partner_id}/venues/{venue_id}/availability
Name
Type
In
Required
Description
partner_id
String
route
Required
Required by the route, then ignored.
venue_id
String
route
Required
The ChiDesk siteId, hyphens optional.
start_date
String
query
Required
First local date to search, ISO 8601.
end_date
String
query
Required
Exclusive end of the search, ISO 8601. Pass one day past the last day you want.
page
Int32
query
Optional
Which page to return. Defaults to 1.
page_size
Int32
query
Optional
How many per page. Defaults to 10.
Responses
Responses for GET /api/v2/classpass/{partner_id}/venues/{venue_id}/availability
Status
Meaning
200
An availability_list object. Practitioners with no free time are omitted.
See the fields
403
Authorization failed. See Errors.
500
venue_id was not a Guid, or a date could not be parsed.
Example
# Monday to Friday: end_date is exclusive, so ask through Saturday.
curl -X GET \
"https://acme.chidesk.com/api/v2/classpass/acme/venues/6f1b8c023a4d4e5f9a012b3c4d5e6f70/availability?start_date=2026-08-03&end_date=2026-08-08" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
ChiDesk creates the client for you when it has never seen the member's e-mail address before, using the name, e-mail, phone, gender and country from the user object. If the address is already on file the existing client is reused, so repeat members do not accumulate duplicates.
The placeholder mobile number +10000000000 is treated as no number and stored blank.
appointment_id is the id ClassPass assigns. ChiDesk converts it into a Guid and uses it as the appointment's service id, which is what makes the later status and attendance calls work - so it has to be the same value you send back on those.
Only the first entry in bookables is used. Sending several does not book several practitioners.
start_datetime is the venue's local time and is converted to UTC on the way in.
Where the site has a source named CLASSPASS, the appointment and any client created here are attributed to it. If no such source exists the booking still succeeds, unattributed - so create one if you want ClassPass business to show up separately in reporting.
Errors on this endpoint come back as 500 with the underlying message in the body, including ones that are really bad input - an unparseable start_datetime returns 500 with "Could not parse start date: ...", not a 400. Read the message, but do not retry a 500 from here without changing the request.
The response echoes the ClassPass appointment_id you sent. It does not return the ChiDesk appointment id.
Parameters
Parameters for POST /api/v2/classpass/{partner_id}/venues/{venue_id}/appointments
var content = new StringContent(
JsonConvert.SerializeObject(appointment), Encoding.UTF8, "application/json");
var response = await http.PostAsync(
"api/v2/classpass/" + partnerId + "/venues/" + venueId + "/appointments",
content);
if ((int)response.StatusCode == 500)
{
// Bad input arrives here too - read the message rather than retrying.
var message = await response.Content.ReadAsStringAsync();
}
response.EnsureSuccessStatusCode();
// { "appointment_id": "cp-9f0a1b2c3d4e5f60" } - the id you sent, not ChiDesk's.
This is the only PATCH in the whole API. A client library that only speaks GET, POST, PUT and DELETE will not reach it.
Only two status values do anything: CANCELLED moves the appointment to the site's cancelled state, and LATE CANCELLED does the same but prefers an inactive state named "Late Cancelled" if the site has one. Both are matched exactly, in upper case.
Any other status value is accepted and changes nothing, and the response is still 200. There is no validation of the value and no way to tell from the response that it was ignored.
An appointment_id that does not resolve returns 404 with "Appointment not found: <id>" - one of very few genuine 404s in this API.
Send the same appointment_id you sent when creating the appointment. ChiDesk converts it back to find the record.
A request with no body fails with a 500, not a 400 - the status is read without a null check.
Parameters
Parameters for PATCH /api/v2/classpass/{partner_id}/venues/{venue_id}/appointments/{appointment_id}
Name
Type
In
Required
Description
partner_id
String
route
Required
Required by the route, then ignored.
venue_id
String
route
Required
The ChiDesk siteId, hyphens optional. Used to resolve the site's cancelled state.
appointment_id
String
route
Required
The ClassPass appointment id used when the appointment was created.
// HttpClient has no PatchAsync on the older frameworks - build the request.
var request = new HttpRequestMessage(
new HttpMethod("PATCH"),
"api/v2/classpass/" + partnerId + "/venues/" + venueId
+ "/appointments/" + classPassAppointmentId)
{
Content = new StringContent(
"{ \"status\": \"CANCELLED\" }", Encoding.UTF8, "application/json")
};
var response = await http.SendAsync(request);
if (response.StatusCode == HttpStatusCode.NotFound)
{
// The appointment id did not resolve.
}
Report whether a ClassPass member attended, for reconciliation after the fact.
Two outcomes, and a missing appointment reads as one of them
There are only two outcomes and no error case. The status is CANCELLED when the appointment is in the site's cancelled state, when it is in a "Late Cancelled" state - and also when no appointment with that id exists at all. Everything else reports ATTENDED, including an appointment that has not happened yet. So a typo in the id reads as a cancellation, and a booking asked about too early reads as attended. Call it only after the appointment should have happened, and confirm the id exists first.
There is no "not found", no "no show" and no "pending" - the two values are the whole vocabulary.
cp_user_id always comes back empty.
appointment_id is echoed back exactly as you sent it.
last_updated is generated at request time, not read from the appointment.
Parameters
Parameters for GET /api/v2/classpass/{partner_id}/venues/{venue_id}/appointments/{appointment_id}/attendance
Name
Type
In
Required
Description
partner_id
String
route
Required
Required by the route, then ignored.
venue_id
String
route
Required
The ChiDesk siteId, hyphens optional.
appointment_id
String
route
Required
The ClassPass appointment id used when the appointment was created.
Responses
Responses for GET /api/v2/classpass/{partner_id}/venues/{venue_id}/appointments/{appointment_id}/attendance
Status
Meaning
200
An attendance_list object holding one attendance record, whose status is either ATTENDED or CANCELLED.
See the fields
403
Authorization failed. See Errors.
Example
curl -X GET \
"https://acme.chidesk.com/api/v2/classpass/acme/venues/6f1b8c023a4d4e5f9a012b3c4d5e6f70/appointments/cp-9f0a1b2c3d4e5f60/attendance" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
The states an appointment can be in - Booked, Arrived, Completed, Cancelled and whatever else the account has defined. Read this to get the state ids you pass to POST /api/v2/appointments/setstate, and to turn a state id in an appointment back into a name you can show.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
siteId is the only parameter, and it is required by name.
State names are per account and can be renamed, so never match on the name. Read the ids once at start-up and cache them.
Every state is returned, whether or not it counts as live. The Active flag on each row is what tells you which is which: a state with Active true is one where the appointment is still going to happen, and false covers the closed-out ones such as cancellations and no-shows.
States not tied to a single site belong to the whole account and are returned for every siteId.
Ordered by the sort order set in the back office, which is the order the account expects to see them in.
The list is cached per account and refreshed when the states change, so it is cheap to call - but do not poll it to detect configuration changes.
Parameters
Parameters for GET /api/v2/appointmentstates
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose states to read.
Responses
Responses for GET /api/v2/appointmentstates
Status
Meaning
200
An array of AppointmentState objects, ordered by sort order.
See the fields
403
Authorization failed. See Errors.
Example
curl -X GET \
"https://acme.chidesk.com/api/v2/appointmentstates?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
// Read once at start-up; the ids are stable, the names are not.
var response = await http.GetAsync(
"api/v2/appointmentstates?siteId=" + siteId);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
// Feed an AppointmentStateId to POST api/v2/appointments/setstate.
Sources
Where business came from - Walk-in, Website, Google, ClassPass, a referral scheme. Put a SourceId on a client or an appointment you create and it shows up in the account's own reporting under that heading.
Endpoint allow-list lines for this category
If the credential you are using has a non-empty endpoint allow-list, these are the
lines that reach the endpoints below. One per line, and remember that routes ending
in a record id cannot be listed at all - see
Allow-lists on a credential.
siteId is the only parameter, and it is required by name.
Only Active sources are returned, and there is no parameter to change that - an inactive source cannot be listed here even though appointments may still reference it. If you meet a SourceId this endpoint does not explain, that is why.
Sources not tied to a single site belong to the whole account and are returned for every siteId.
Ordered by name.
The list is cached per account and refreshed when the sources change, so it is cheap to call.
Ask the account to add a source for your integration and tag what you create with it. That is what makes your bookings separable in their reporting - and it is how the ClassPass integration attributes its own.
Parameters
Parameters for GET /api/v2/sources
Name
Type
In
Required
Description
siteId
Guid
query
Required
The site whose sources to read.
Responses
Responses for GET /api/v2/sources
Status
Meaning
200
An array of SourceList objects, ordered by name.
See the fields
403
Authorization failed. See Errors.
Example
curl -X GET \
"https://acme.chidesk.com/api/v2/sources?siteId=6f1b8c02-3a4d-4e5f-9a01-2b3c4d5e6f70" \
-H "Authorization: Bearer $CHIDESK_TOKEN"
var response = await http.GetAsync("api/v2/sources?siteId=" + siteId);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
// Put the SourceId on the clients and appointments you create.
Object schemas
The shape of every object the API returns or accepts. Each endpoint's Responses table
links to the schema it returns, so you can go from a call to the fields it gives you
without leaving the page.
These tables list what the JSON actually carries. A few properties exist in ChiDesk's
own code but are never serialized, and those are called out on the schemas that have
them rather than listed as fields you can read. Where a field's type is another object
documented here, the type links to it instead of repeating the table - the object
graph contains cycles, so it cannot be expanded inline.
One row of the diary. Returned by GET /api/v2/appointments - note that this is one row per booked service, not one per appointment, so an appointment with three services appears three times sharing an AppointmentId.
This type carries a TotalInvoicesNumeric property in C# that never appears in the JSON - it has no [DataMember] and the type is opt-in. Read TotalInvoices and treat null as zero yourself.
Fields of the AppointmentList object
Field
Type
Null
Description
AppointmentId
Guid
No
The appointment this row belongs to. Repeats across every service row of the same appointment.
AppointmentStateId
Guid
No
Current state. Resolve against the Appointment states reference.
AppointmentState
String
Yes
The state's display name, in the account's language.
AppointmentStateColour
String
Yes
The state's diary colour as a hex string.
SiteId
Guid
No
The site whose diary this row came from.
ClientId
Guid
Yes
The client the appointment is billed to. Null for a walk-in with no client record.
ClientName
String
Yes
Billed client's display name.
Phone
String
Yes
Billed client's phone number.
Email
String
Yes
Billed client's email address.
AppointmentClientId
Guid
Yes
The client actually receiving the service, where that differs from the billed client - a booking made on someone else's behalf.
AppointmentClientName
String
Yes
Display name of the receiving client.
AppointmentPhone
String
Yes
Phone number of the receiving client.
AppointmentNumber
String
Yes
The human-readable appointment reference shown in the back office.
CreationDate
DateTime
No
When the appointment was created, UTC.
StartTime
DateTime
Yes
Start of this service, UTC.
EndTime
DateTime
Yes
End of this service, UTC.
AppointmentServiceId
Guid
No
Identifies this specific service row - the value to use when you need to act on one service rather than the whole appointment.
Service
String
Yes
Name of the booked service.
ServiceType
String
Yes
The service's type/category name.
UserName
String
Yes
The back-office user who created the appointment.
VIP
Boolean
Yes
Whether the client is flagged VIP. Null where no client is attached.
Notes
String
Yes
Free-text notes on the appointment.
RatingScore
Int32
Yes
The client's rating of this appointment, where one was captured.
TotalInvoices
Decimal
Yes
Invoiced value against the appointment. Null where nothing has been invoiced - not zero.
TotalReceipts
Decimal
No
Value received against the appointment.
EmployeeNames
String[]
Yes
Employees assigned to this service, one entry each.
Employees
String
Yes
The same employees pre-joined into a single display string.
ResourceNames
String[]
Yes
Rooms or equipment assigned to this service.
Resources
String
Yes
The same resources pre-joined into a single display string.
TotalServices
Decimal
No
Value of the services on the appointment.
StartEndString
String
Yes
Pre-formatted "09:00-10:30" for display. Empty string when either time is null.
StartString
String
Yes
Pre-formatted start date, "Mon, 3 Aug 2026".
CreationDateString
String
Yes
Pre-formatted creation date and time.
StartDateString
String
Yes
Pre-formatted long start date, "Mon, 03 August 2026".
StartDate
DateTime
Yes
Start time with the time component stripped - the date the appointment falls on.
A whole appointment and the services booked on it. Returned by GET /api/v2/appointments/{appointmentId}, and the shape POST and PUT /api/v2/appointments accept.
This is the nested shape: one appointment carrying its AppointmentServices. GET /api/v2/appointments returns something different - AppointmentList, one flat row per booked service. Do not expect the two to have the same fields. Note also that Rating here is a String, while AppointmentService.Rating is an Int32.
Fields of the Appointment object
Field
Type
Null
Description
AppointmentId
Guid
No
The appointment's id. Send an empty Guid when creating.
SiteId
Guid
No
The site the appointment belongs to.
ClientId
Guid
Yes
The client the appointment is billed to. Null for a walk-in with no client record.
ClientEmail
String
Yes
The client's email address, carried on the appointment itself.
Room
String
Yes
Hotel room number, on accounts integrated with a PMS.
ClientTypeId
Guid
Yes
The client type the appointment was booked under.
AppointmentNumber
String
Yes
The human-readable appointment reference. ChiDesk assigns this - do not invent one when creating.
One service booked on an appointment - who is doing it, where, and when. Carried inside Appointment, and the shape the cartItems body takes on POST /api/v2/appointments/validate and availabletimes.
IsPreferredEmployee is always false on an API response. It is computed only by the back-office diary query, not by the fetch this endpoint uses, and its setter is internal so sending it has no effect either. Treat it as absent. Note also that the four Prep and Recovery fields are why a service's Duration and the diary block it occupies are different numbers - that is by design, not a bug.
Fields of the AppointmentService object
Field
Type
Null
Description
AppointmentServiceId
Guid
No
This service row's id - the value to use when acting on one service rather than the whole appointment.
Groups the service rows that came from the same package purchase.
Start
DateTime
No
Start of this service, UTC.
End
DateTime
No
End of this service, UTC. Covers the treatment time only - see the Prep and Recovery fields.
Description
String
Yes
The service's description as it appears on the diary block.
Rating
Int32
Yes
The client's numeric rating of this service. An Int32 here, unlike Appointment.Rating which is a String.
CalendarColour
String
Yes
Diary colour for this row as a hex string.
AppointmentNotes
String
Yes
Notes on this service row, distinct from the appointment's own Notes.
VIP
Boolean
No
Whether the receiving client is flagged VIP.
Member
Boolean
No
Whether the receiving client is on a membership.
TotalInvoices
Decimal
No
Invoiced value against this service.
TotalReceipts
Decimal
No
Value received against this service.
Invoiced
Boolean
No
Whether this service has been invoiced. Check this rather than comparing TotalInvoices to zero - a zero-value service can still be invoiced.
ResourceIds
String
Yes
The assigned resource ids as a single delimited string, for the back-office editor. Read AppointmentResourceIds instead.
AppointmentResourceIds
Guid[]
Yes
The assigned resource ids as an array. This is the machine-readable form.
EmployeeNames
String
Yes
Assigned employees as "First Last" joined with ", ". Built by the fetch from the Employees list.
ResourceNames
String
Yes
Assigned resource names joined with ", ". Built by the fetch from the Resources list.
Employees
Employee[]
Yes
The assigned employees as full Employee objects - a larger shape than EmployeeList, carrying the employee's own working hours and sites. This is where the object graph becomes cyclic, so it is not expanded here.
Resources
Resource[]
Yes
The assigned rooms or equipment. Each Resource carries ResourceId, SiteId, Name, State, Description, DefaultEmployeeId, ShowOnCalendar, ShowOnClasses, CalendarSortOrder, MaxPax and MaxPaxPerAppointment.
EmployeePrep
Int32
No
Minutes an employee is reserved before the service starts.
EmployeeRecovery
Int32
No
Minutes an employee is reserved after it ends.
ResourcePrep
Int32
No
Minutes the room or equipment is reserved beforehand - cleaning, setup.
One slot a booking could be made in, with the staff and rooms that would be assigned to it. Returned by GET and POST /api/v2/appointments/availabletimes.
EmployeesString and ResourcesString are the Employees and Resources arrays already run through a JSON serializer, so they arrive as a STRING containing JSON - you would have to parse them a second time. They exist for the back-office booking screen. Read the Employees and Resources arrays instead and ignore both string fields.
Fields of the AvailableTime object
Field
Type
Null
Description
Start
DateTime
No
Start of the available slot, UTC. This is the value to send back when creating the appointment.
StartTime
String
Yes
Start pre-formatted for display. Built with a 24-hour hour token followed by an AM/PM marker, so it reads like "14:30 PM" in the afternoon - display it as-is at your own risk, and format Start yourself instead.
End
DateTime
No
End of the slot, UTC.
EndTime
String
Yes
End pre-formatted the same way, with the same quirk.
Employees
AvailableEmployee[]
Yes
Which employees would be assigned. Each entry carries EmployeeId, PackageItemId (set when the slot is for one item of a package) and ServiceIndex (which service in the requested sequence it applies to).
Resources
AvailableResource[]
Yes
Which rooms or equipment would be assigned, in the same shape: ResourceId, PackageItemId and ServiceIndex.
EmployeesString
String
Yes
The Employees array as a JSON string. See the note above.
ResourcesString
String
Yes
The Resources array as a JSON string. See the note above.
One block of an employee's roster - a shift, a break, leave, or a class they are taking. Returned by GET /api/v2/employees/schedules.
The text form of the type is spelled SchedulerTypeName on the wire, not ScheduleTypeName - the extra "r" is in ChiDesk's own property name and is what the JSON carries. Match on the numeric ScheduleType instead: 0 Shift, 1 Unavailable, 2 SickLeave, 3 Leave, 4 FamilyLeave, 5 MaternityLeave, 6 UnpaidLeave, 7 Off, 8 PublicHoliday, 9 Break, 10 Training, 99 Teaching. Note that Teaching is 99, not 11.
One clock-in/clock-out pair for a member of staff. Returned by GET /api/v2/timerecords.
This is the list shape, so the employee arrives as a name string only - there is no EmployeeId on it. Match staff up by name, or read the roster from GET /api/v2/employees/schedules instead, which does carry ids.
Fields of the TimeRecordList object
Field
Type
Null
Description
TimeRecordId
Guid
No
The time record's id.
Employee
String
Yes
The employee's display name. No id is returned on this shape.
StartTime
DateTime
No
When the employee clocked in, UTC.
EndTime
DateTime
Yes
When they clocked out, UTC. Null while they are still clocked in - so a null here means an open shift, not missing data.
The full record for one location, including how it formats money, dates and numbers. Returned by GET /api/v2/sites/{siteId}.
Two differences from SiteList worth planning for. The address arrives here as a nested Address object rather than flattened into Street/City/Country fields, so the same data sits at a different path. And this shape carries the site's currency, date, time and number formats, which the list does not - fetch a site once at startup if you need to render its values the way the back office does.
Fields of the Site object
Field
Type
Null
Description
SiteId
Guid
No
The site's id.
State
Byte
No
Item state as a number - 1 Active, 2 Inactive. SiteList does not return this.
Name
String
Yes
Site name.
Code
String
Yes
The account's own code for the site.
CurrencyCode
String
Yes
Currency the site trades in, e.g. "ZAR". Every money field on every endpoint scoped to this site is in this currency - the API does not tag amounts individually.
DateFormat
String
Yes
How the site formats dates in the back office. Does not affect the API, which is ISO 8601 throughout.
TimeFormat
String
Yes
How the site formats times in the back office.
NumberFormat
String
Yes
How the site formats numbers in the back office.
TimeZone
String
Yes
Windows time-zone id. Appointment and document times are UTC, so this is what you convert with for display.
TaxRegistered
Boolean
No
Whether the site is registered for tax. Where false, the tax figures on documents are zero by definition rather than by omission.
TaxNumber
String
Yes
The site's tax or VAT number.
RegistrationNumber
String
Yes
Company registration number.
Email
String
Yes
Contact email address.
Phone
String
Yes
Contact phone number.
Fax
String
Yes
Fax number.
Web
String
Yes
Website address.
MainSite
Boolean
No
True on the account's main site. Endpoints that default a missing siteId use this one.
ContactDetails
String
Yes
Free-text contact block as printed on documents.
BankingDetails
String
Yes
Free-text banking block as printed on invoices.
MedicalDetails
String
Yes
Free-text practice details, for accounts billing medical aids.
One client as it appears in a list or search result. Returned by GET /api/v2/clients.
This is the list shape, not the full record - it carries no addresses, no notes, no consent flags and no custom fields. Fetch GET /api/v2/clients/{clientId} for those. Note also that Image is on this shape: a list of clients with photos is a large response, and there is no parameter to suppress it.
Fields of the ClientList object
Field
Type
Null
Description
ClientId
Guid
Yes
The client's id. De-duplicate on this - an account-wide client is returned by every site's list.
SiteId
Guid
Yes
The site the client belongs to. Null on an account-wide client.
Name
String
Yes
Full display name, first and last joined.
FirstName
String
Yes
First name.
LastName
String
Yes
Last name. The list comes back ordered by this.
Code
String
Yes
The account's own code for the client.
Email
String
Yes
Email address.
Gender
Byte
Yes
Gender code, where recorded.
BirthMonth
Int32
Yes
Month of birth, 1-12. Held separately from the day and year because a client may give only part of a birthday.
BirthDay
Int32
Yes
Day of the month of birth.
BirthYear
Int32
Yes
Year of birth. Commonly null even where month and day are set - do not compute an age without checking.
The full client record. Returned by GET /api/v2/clients/{clientId}, and the shape POST and PUT /api/v2/clients accept.
Two rows to read before you size a client response: ClientDocuments carries whole files inline, which is what makes this response large, and ClientCourses is already filtered to unused sessions rather than being a full history. To check a client's password, use GET /api/v2/clients/authenticate.
Fields of the Client object
Field
Type
Null
Description
ClientId
Guid
No
The client's id. Send an empty Guid when creating.
State
Byte
No
Item state as a number - 1 Active, 2 Inactive.
SiteId
Guid
Yes
The site the client belongs to. Null makes the client account-wide.
Another client record that represents this client's company.
InvoiceCompany
Boolean
No
Whether sales are invoiced to CompanyClientId instead of this client.
AccountPassword
String
Yes
Create-time only: send a portal password here on POST to give the client one, and ChiDesk stores only a hash of it. Leave it out and the client has no password. On update use NewAccountPassword instead - a value sent here on a PUT is written to the record as sent, and does not change the portal password.
NewAccountPassword
String
Yes
Write-only in practice: set this on a PUT to change the portal password. Null on a read.
AccountVerified
Boolean
No
Whether the client has verified their email address.
Marketing consent. GET /api/v2/clients/contacts returns only clients where this is true, and no parameter overrides that.
ReceiveReminders
Boolean
No
Whether the client wants appointment reminders. Separate from marketing consent.
ReceiveCourseNotifications
Boolean
No
Whether the client wants course-related notifications.
ContactSms
Boolean
No
Whether the client accepted SMS. What contactSms filters on.
ContactEmail
Boolean
No
Whether the client accepted email. What contactEmail filters on.
ContactTelephone
Boolean
No
Whether the client accepted phone calls. What contactPhone filters on - note the parameter is named contactPhone but the field is ContactTelephone.
AccountBalance
Decimal
No
The client's account balance.
TermsAccepted
DateTime
Yes
When the client accepted the account's terms. Null where they have not.
OTPExpiry
DateTime
Yes
When the client's current one-time code expires. Null where no code is outstanding.
Image
Byte[]
Yes
Profile photo as raw bytes, base64-encoded in the JSON.
ClientNotes
ClientNote[]
Yes
Notes on the client. Each entry carries ClientNoteId, ClientId, CreationDate, ClientNoteType, Body, Category, UserId, Encrypted and ObjectState. Where Encrypted is true the Body is ciphertext, not readable text.
ClientDocuments
ClientDocument[]
Yes
Files attached to the client. Each entry carries a Contents byte array holding the WHOLE file inline, base64-encoded - this is what makes a client response large. Also Name, Category, Filename, Url, Notes and ObjectState.
The client's course sessions, ALREADY FILTERED to those not yet redeemed - a used session is absent, so treat this as the remaining balance rather than a history. Each entry carries ClientCourseId, CourseId, CourseServiceId, CourseServiceName, CourseNumber, the buying and redeeming document ids and numbers, ExpiryDate and ObjectState.
ClientCustomFieldValues
ClientCustomFieldValue[]
Yes
Values for the account's own client fields. Each entry carries ClientId, CustomFieldId and Value - resolve the field ids from the back office.
A marketing-list row: a client who has agreed to be contacted. Returned by GET /api/v2/clients/contacts.
The consent flags themselves are NOT on this shape. The endpoint filters by channel, but the rows carry no ContactEmail, ContactSms or ContactTelephone, so a response cannot tell you which channel a given client accepted - only that they passed the filter you asked for. Query one channel at a time if you need to know, or read the flags from the full Client record.
Fields of the ClientContact object
Field
Type
Null
Description
ClientId
Guid
Yes
The client's id.
Name
String
Yes
Full display name.
Title
String
Yes
Salutation, for addressing the message.
FirstName
String
Yes
First name.
LastName
String
Yes
Last name. The list comes back ordered by this.
Code
String
Yes
The account's own code for the client.
Email
String
Yes
Email address. Null where the client has none on file, even when they accepted email.
Category
String
Yes
Client category name - useful for segmenting the list.
One past service a client received, for building a treatment history. Returned by GET /api/v2/clients/appointmenthistory - one row per service, so a visit with three services appears three times sharing an AppointmentId.
This type carries an AccountingDocumentId property in C# that never appears in the JSON - it has no [DataMember] and the type is opt-in. You get InvoiceNumber but not the invoice's id, so to reach the document itself match the number against GET /api/v2/accountingdocuments rather than expecting an id here.
Fields of the AppointmentHistory object
Field
Type
Null
Description
AppointmentId
Guid
No
The appointment this service belonged to. Repeats across every service row of the same appointment.
AppointmentNumber
String
Yes
The human-readable appointment reference.
AppointmentDate
DateTime
No
When the service was delivered, UTC.
ServiceName
String
Yes
Name of the service received.
Duration
Int32
Yes
Treatment time in minutes. Null where the service no longer carries one.
AppointmentStateName
String
Yes
The appointment's state as text, in the account's language. Only the name is returned here - no state id and no colour, unlike AppointmentList.
InvoiceNumber
String
Yes
Number of the invoice the service was billed on. Null where it was never invoiced.
EmployeeNames
String[]
Yes
Employees who delivered the service, one entry each.
Employees
String
Yes
The same employees pre-joined into a single display string.
One completed custom form - a consultation card, intake questionnaire or consent form - with the client's answers on it. Returned by GET /api/v2/clientcustomforms.
The answers are keyed by CustomFormFieldId, and this endpoint does not return the form definition those ids belong to. You need the field ids from the back office before the values mean anything. Each answer carries both Value and Values - single-choice and free-text answers populate Value, multi-select answers populate Values, and the other one is null.
Fields of the ClientCustomForm object
Field
Type
Null
Description
ClientCustomFormId
Guid
No
This submission's id.
ClientId
Guid
Yes
The client who completed the form. Nullable on the type, though the back office requires it.
SiteId
Guid
No
The site the form was captured at.
UserId
Guid
Yes
Back-office user who captured it. Null where the client completed it themselves.
CustomFormNumber
String
Yes
The human-readable reference for this submission.
CustomFormId
Guid
No
Which form definition was filled in. The same value across every submission of that form - this is not the submission's own id.
Signature
String
Yes
The client's captured signature, as a data string. Null where the form was not signed - expect this field to be large when it is populated.
ClientCustomFormValues
ClientCustomFormValue[]
Yes
The answers. Each entry carries ClientCustomFormId, CustomFormFieldId, Value (String) and Values (String array). See the note above.
A bookable service as it appears in the catalogue. Returned by GET /api/v2/services.
SpecialValidity exists on this class in C# but is never serialized - it has no [DataMember] and the type is opt-in. Do not expect it in the response.
Fields of the ServiceList object
Field
Type
Null
Description
ServiceId
Guid
Yes
The service's id.
Name
String
Yes
Display name.
Category
String
Yes
Catalogue category name.
Code
String
Yes
The account's own service code.
BarCode
String
Yes
Barcode, where one is set.
Duration
Int32
No
Client-facing treatment time in minutes. This is not the diary block - the diary also reserves preparation and recovery time, so a service shown here as 60 can occupy more of the diary.
StartTime
DateTime
Yes
Fixed start time, for services that only run at a set time.
ServiceTypeId
Guid
No
The service type this belongs to.
ServiceTypeName
String
Yes
The service type's display name.
ServiceTypeAccount
String
Yes
Ledger account the service type posts to.
Cost
Decimal
No
Cost price.
SellInclusive
Decimal
Yes
Selling price including tax.
Commission
Decimal
Yes
Commission value or percentage, per the account's commission setup.
LoyaltyPoints
Decimal
No
Loyalty points earned on this service.
Description
String
Yes
Long description, as shown to clients online.
CalendarColour
String
Yes
Diary colour as a hex string.
SellOnline
Boolean
No
Whether the service is bookable through online booking.
SellVoucher
Boolean
No
Whether the service can be sold as a voucher.
Special
Boolean
No
Whether the service is currently on special.
PackageService
Boolean
No
Whether this service exists only as part of a package. GET /api/v2/services excludes these unless showPackageServices=true.
RequiredEmployees
Int32
No
How many employees the service needs booked against it.
RequiredResources
Int32
No
How many resources (rooms, equipment) the service needs.
Several items sold together as one product. Returned by GET /api/v2/packages.
SpecialValidity exists on this class in C# but is never serialized - it has no [DataMember] and the type is opt-in.
Fields of the PackageList object
Field
Type
Null
Description
PackageId
Guid
Yes
The package's id.
Name
String
Yes
Display name.
Code
String
Yes
The account's own package code.
Category
String
Yes
Catalogue category name.
BarCode
String
Yes
Barcode, where one is set.
Duration
Int32
No
Client-facing treatment time in minutes. As with a service, this is not the diary block - the diary also reserves preparation and recovery time, so the two numbers differ by design.
StartTime
DateTime
Yes
Fixed start time, for packages that only run at a set time.
Cost
Decimal
No
Cost price.
SellInclusive
Decimal
No
Selling price including tax.
Commission
Decimal
No
Commission value or percentage, per the account's commission setup.
LoyaltyPoints
Decimal
No
Loyalty points earned on the package.
Image
Byte[]
Yes
Package image as raw bytes, base64-encoded in the JSON. Null where none is set.
FirstItemTypeId
Byte
No
Item type of the first item in the package - what the package leads with.
Description
String
Yes
Long description, as shown to clients online.
SellOnline
Boolean
No
Whether the package is bookable through online booking.
A membership product clients can subscribe to. Returned by GET /api/v2/memberships.
SpecialValidity exists on this class in C# but is never serialized - it has no [DataMember] and the type is opt-in. The membership's rules - expiry, booking limits, activation - are not on this list shape either; it is the catalogue view.
Fields of the MembershipList object
Field
Type
Null
Description
MembershipId
Guid
Yes
The membership's id.
Name
String
Yes
Display name.
Category
String
Yes
Catalogue category name.
Description
String
Yes
Long description, as shown to clients online.
Code
String
Yes
The account's own membership code.
Cost
Decimal
No
Cost price.
SellInclusive
Decimal
Yes
Selling price including tax.
Commission
Decimal
Yes
Commission value or percentage, per the account's commission setup.
LoyaltyPoints
Decimal
No
Loyalty points earned on the membership.
SellOnline
Boolean
No
Whether the membership can be bought through online booking.
A class in the catalogue - the definition, not a scheduled occurrence of it. Returned by GET /api/v2/classes.
Do not confuse this with ClassScheduleList. This is the class as a sellable thing; a ClassScheduleList row is one dated, staffed instance of it that clients actually book.
Fields of the ClassList object
Field
Type
Null
Description
ClassId
Guid
No
The class's id.
Name
String
Yes
Display name.
ClassLevel
String
Yes
The class level's display name, e.g. Beginner.
Category
String
Yes
Catalogue category name.
Start
DateTime
No
First date the class runs from.
End
DateTime
No
Last date the class runs to.
StartTime
DateTime
No
Time of day the class starts.
StartTimeString
String
Yes
StartTime pre-formatted as a short time in the account's culture, for display. Match on StartTime.
The full class definition, including the weekly pattern its occurrences are generated from. The shape POST and PUT /api/v2/classes accept.
This is where a class's recurrence lives: the seven day flags plus StartTime and EndTime describe the weekly pattern, and Start and End bound the period it runs over. ChiDesk generates the dated occurrences from that - you do not create ClassSchedules yourself. ClassList, returned by GET /api/v2/classes, is a much smaller catalogue shape and carries none of this.
Fields of the Class object
Field
Type
Null
Description
ClassId
Guid
No
The class's id. Send an empty Guid when creating.
State
Byte
No
Item state as a number - 1 Active, 2 Inactive.
SiteId
Guid
No
The site the class belongs to. Required in the body on a PUT - it decides which site the record is saved against.
ClassType
Byte
No
The class type as a number. The same set the classType query parameter accepts elsewhere.
The generated occurrences. Each entry carries ClassScheduleId, Location, ClassId, ClassName, ClassDescription, ClassType, ClassServiceId, SiteId, ClassScheduleState, Start, End, the employee and resource with their names, MaxPax, BookOnline, MaxOnlineBookings and its own ClassBookings. Note this is a different shape from ClassScheduleList, which the read endpoint returns.
One client's place in a scheduled class, and how it was paid for.
ClassBookingStateName exists on this class in C# but is never serialized - it is a computed getter with no [DataMember] on an opt-in type. Resolve ClassBookingState yourself: 0 Booked, 1 Cancelled, 2 CheckedIn, 3 LateCancelled, 4 NoShow.
One accounting document - an invoice, credit note, quotation or one of the purchasing documents. Returned by GET /api/v2/accountingdocuments.
A document says what was owed; the entries against it say what was paid. The payment side is summarised here in the four parallel AccountingEntry arrays, which are index-aligned with each other - entry n's id, payment type, number and total share position n.
Fields of the AccountingDocumentList object
Field
Type
Null
Description
AccountingDocumentId
Guid
No
The document's id.
DocumentDate
DateTime
No
The document's own date - what it is reported against.
DocumentNumber
String
Yes
The document number shown to the client or supplier.
ClientSupplierId
Guid
Yes
The client or supplier the document is for, depending on its type.
ClientSupplierName
String
Yes
That party's display name.
Reference
String
Yes
Free-text reference on the document.
AccountingEntryIds
Guid[]
Yes
Ids of the entries paid against this document. Index-aligned with the three arrays below.
AccountingEntryPaymentTypes
String[]
Yes
Payment type of each entry, in the same order as AccountingEntryIds.
AccountingEntryNumbers
String[]
Yes
Number of each entry, in the same order.
AccountingEntryTotals
Decimal[]
Yes
Total of each entry, in the same order. Sum these to see how much of the document has been settled.
AccountingEntries
String
Yes
The same entries pre-joined into a single display string.
The full document with its line items. Returned by GET /api/v2/accountingdocuments/{accountingDocumentId}, and the shape POST and PUT accept. Also the payload of the invoice, credit note, quotation and purchasing WebHook events.
The counterparty is split here into separate ClientId and SupplierId fields, where AccountingDocumentList collapses both into one ClientSupplierId. Which one is populated follows AccountingDocumentTypeId - a client invoice carries ClientId, a purchase order carries SupplierId. Note too that this shape carries no payment information at all: the AccountingEntry arrays exist only on the list shape.
Fields of the AccountingDocument object
Field
Type
Null
Description
AccountingDocumentId
Guid
No
The document's id. Send an empty Guid when creating.
SiteId
Guid
No
The site the document belongs to. Required in the body - it decides which site the record is saved against.
ClientId
Guid
Yes
The client, on a client-facing document. Null on a purchasing document.
SupplierId
Guid
Yes
The supplier, on a purchasing document. Null on a client-facing one.
DestinationSiteId
Guid
Yes
The receiving site on a stock transfer. Null on every other document type.
DocumentNumber
String
Yes
The document number shown to the client or supplier. ChiDesk assigns this.
The document this one relates to - a credit note points at the invoice it credits.
LinkedAccountingDocumentTypeId
Byte
Yes
That document's type, so you can tell what it is without fetching it.
AccountingDocumentTypeId
Byte
No
What kind of document this is, as a number. Required when creating - it decides nearly everything else about how the document behaves.
DocumentDate
DateTime
No
The document's own date - what it is reported against.
ExpiryDate
DateTime
Yes
When the document lapses. Used by quotations.
DueDate
DateTime
Yes
When payment falls due.
Total
Decimal
No
Document total.
UserId
Guid
Yes
Back-office user who raised it.
CashDrawerId
Guid
No
Cash drawer the document was put through. Required.
Room
String
Yes
Hotel room number, on accounts integrated with a PMS.
AccountingDocumentItems
AccountingDocumentItem[]
Yes
The line items. Each carries AccountingDocumentItemId, Description, TaxId, Quantity, Cost, SellInclusive, SellExclusive, DiscountPercentage, the Total and Commission pairs, LoyaltyPoints, ItemType, the Service/Product/Voucher/Course/Membership id that was sold, SortOrder, SourceId, ClassBookingId, the client, package ids, activation and expiry dates, its Appointments, Employees and MedicalCodes with their joined-string equivalents, and a computed DiscountAmount.
CouponId
Guid
Yes
The coupon applied to the document, where one was.
CouponReason
String
Yes
Why the coupon was applied.
PostedDate
DateTime
Yes
When the document was posted to an external accounting or PMS system. Null where it has not been posted.
PostedCode
String
Yes
The external system's reference for the posting.
Recurrence
Recurrence
Yes
Set on a recurring document, describing the schedule that generates it: RecurrenceId, Name, SendEmails, NotifyOnEnd, Interval, Day, IntervalAmount, DayOfWeek, Month, Start, End, Occurrences, DaysInAdvance, PreviousDate and NextDate.
RecurrenceCreationId
Guid
Yes
Set on a document that was generated by a recurrence, pointing back at the run that created it.
One movement of money - a receipt or a payment. Returned by GET /api/v2/accountingentries.
An entry is not tied to exactly one document. AccountingDocumentId is nullable, so an entry can exist without one; reconcile from the document side using its AccountingEntry arrays rather than assuming a one-to-one match.
Fields of the AccountingEntryList object
Field
Type
Null
Description
AccountingEntryId
Guid
No
The entry's id.
EntryDate
DateTime
No
Date of the movement.
EntryNumber
String
Yes
The entry number.
ClientSupplierId
Guid
No
The client or supplier the money moved to or from.
The full record of one movement of money. Returned by GET /api/v2/accountingentries/{accountingEntryId}, and the shape POST and PUT accept. Also the payload of the receipt and payment WebHook events.
As with documents, the counterparty is split into ClientId and SupplierId here where the list shape collapses them into ClientSupplierId. The money fields are not interchangeable: Received is what was handed over, Change is what was given back, and Total is the net of the two - reconcile on Total, not Received.
Fields of the AccountingEntry object
Field
Type
Null
Description
AccountingEntryId
Guid
No
The entry's id. Send an empty Guid when creating.
SiteId
Guid
No
The site the entry belongs to. Required in the body.
ClientId
Guid
Yes
The client, on a receipt. Null on a supplier payment.
SupplierId
Guid
Yes
The supplier, on a payment. Null on a client receipt.
EntryNumber
String
Yes
The entry number. ChiDesk assigns this.
Reference
String
Yes
Free-text reference.
Description
String
Yes
Free-text description.
EntryDate
DateTime
No
Date of the movement.
AccountingEntryTypeId
Byte
No
Entry type as a number: 1 Receipt, 2 Payment, 10 RecurringReceipt, 11 RecurringPayment. Required when creating.
The voucher redeemed, where the payment was by voucher.
UserId
Guid
Yes
Back-office user who processed the entry.
CashDrawerId
Guid
No
Cash drawer the entry was put through.
SelectedGratuityEmployees
Guid[]
Yes
Employees the gratuity is being split between, as sent by the back office when capturing the entry. An input convenience - read Gratuities for what was actually recorded.
Gratuities
AccountingEntryGratuity[]
Yes
The gratuity split as recorded. Each entry carries AccountingEntryId, EntryDate, EntryNumber, the employee and their name, DateSettled, the user, payment type and cash drawer with their names, and the Amount.
PostedDate
DateTime
Yes
When the entry was posted to an external accounting or PMS system. Null where it has not been posted.
PostedCode
String
Yes
The external system's reference for the posting.
Recurrence
Recurrence
Yes
Set on a recurring entry, describing the schedule that generates it. Same shape as on AccountingDocument.
RecurrenceCreationId
Guid
Yes
Set on an entry generated by a recurrence, pointing back at the run that created it.
ChargedDate
DateTime
Yes
When the payment was actually taken, for entries collected by debit order or stored card.
The full voucher record. Returned by GET /api/v2/vouchers/{voucherId}, and the shape POST and PUT /api/v2/vouchers accept.
Remaining is computed as Amount less UsedAmount, so it is derived rather than stored - send Amount and UsedAmount when writing, not Remaining. This shape carries the recipient and delivery detail that VoucherList omits, but not the invoice the voucher was sold on, which only VoucherList has.
Fields of the Voucher object
Field
Type
Null
Description
VoucherId
Guid
No
The voucher's id. Send an empty Guid when creating.
SiteId
Guid
Yes
The site the voucher belongs to. Required in the body - omitting it causes a 500 on update.
State
Byte
No
Item state as a number - 1 Active, 2 Inactive.
VoucherType
Byte
No
What kind of voucher this is, as a number. Required.
VoucherNumber
String
Yes
The number printed on the voucher and quoted by the client. Must be unique on the account.
CreationDate
DateTime
No
When the voucher was created, UTC.
ExpiryDate
DateTime
No
When it expires, UTC.
DeliveryDate
DateTime
Yes
When the voucher is to be delivered to its recipient. Null for immediate delivery.
ActivationDate
DateTime
Yes
When it was activated. Null while it is sold but not yet active - an inactive voucher still has a Remaining balance, so check this before accepting it.
ClientId
Guid
Yes
The client who bought it. Null where it was sold without a client record.
One sold line item, with its money broken out. Returned by GET /api/v2/reports/sales - one row per item sold, not one per document.
This is a reporting shape, so it denormalises heavily: the site, client, user, document and item all appear on every row. Group by AccountingDocumentId to get back to documents.
Fields of the Sales object
Field
Type
Null
Description
AccountingDocumentItemId
Guid
No
The line item's id - the only field unique per row.
SiteId
Guid
No
Site the sale belongs to.
Site
String
Yes
That site's name.
ClientId
Guid
Yes
Client the sale was to. Null for a cash sale with no client record.
Client
String
Yes
That client's display name.
ClientCode
String
Yes
The account's own code for the client.
Gender
Byte
Yes
The client's gender code, where recorded.
UserId
Guid
Yes
Back-office user who processed the sale.
User
String
Yes
That user's name.
CashDrawerId
Guid
No
Cash drawer the sale was put through.
CreationDate
DateTime
No
When the row was created, UTC.
DocumentDate
DateTime
No
The document's own date - what the sale is reported against. This is the one to bucket by, not CreationDate.
AccountingDocumentId
Guid
No
The document this line belongs to.
DocumentNumber
String
Yes
That document's number.
Code
String
Yes
The sold item's code.
BarCode
String
Yes
The sold item's barcode.
Description
String
Yes
The sold item's description as it appears on the document.
Reference
String
Yes
Free-text reference on the line.
Quantity
Decimal
No
How many were sold. Can be negative on a credit note.
TaxId
Guid
Yes
The tax rate applied.
Cost
Decimal
No
Cost of the item, used for the profit figure.
Discount
Decimal
No
Discount given on the line.
Loyalty
Decimal
No
Loyalty value applied to the line.
Tax
Decimal
No
Tax charged on the line.
Profit
Decimal
Yes
TotalExclusive less Cost. Null where cost is not known.
TotalInclusive
Decimal
No
Line total including tax.
TotalExclusive
Decimal
No
Line total excluding tax.
CommissionInclusive
Decimal
No
Commission on the line including tax.
CommissionExclusive
Decimal
No
Commission on the line excluding tax.
VoucherRedemptions
Decimal
No
Value redeemed against a voucher on this line.
ItemTypeId
Byte
No
What kind of thing was sold, as a number. ItemType carries the same value as text.
ItemType
String
Yes
The item type resolved to text in the account's language. Match on ItemTypeId.
SupplierId
Guid
Yes
Supplier of the item, for products.
Supplier
String
Yes
That supplier's name.
Brand
String
Yes
Brand of the item, for products.
Category
String
Yes
Catalogue category of the item.
Source
String
Yes
How the sale originated - resolve against the Sources reference.
Room
String
Yes
Room the service was delivered in.
PostedDate
DateTime
Yes
When the line was posted to an external accounting or PMS system. Null where it has not been posted.
One alert definition configured on the account - a flag that can be raised against a client or an appointment. Returned by GET /api/v2/alerts.
Selected is always false on an API response. It is the back-office editor's checkbox state, not an indication that the alert is set on anything - reading it as "this alert applies" is wrong for every row.
Fields of the Alert object
Field
Type
Null
Description
AlertId
Guid
No
The alert definition's id.
Name
String
Yes
Display name.
Category
String
Yes
Category the alert is grouped under. The list comes back ordered by this, then by name.
SiteId
Guid
Yes
The site the alert belongs to. Null on an account-wide alert, which is returned for every siteId you ask for.
One appointment state the account has configured - Booked, Arrived, Completed and whatever else the account has added. Returned by GET /api/v2/appointmentstates.
States are per-account data, not a fixed enum. Do not hard-code the names or assume a particular set exists: read this list and resolve the AppointmentStateId you get on an appointment against it. The same id is what POST /api/v2/appointments/setstate takes.
Fields of the AppointmentState object
Field
Type
Null
Description
AppointmentStateId
Guid
No
The state's id. This is the value carried on AppointmentList.AppointmentStateId and accepted by setstate.
Name
String
Yes
Display name, in the account's language.
Active
Boolean
No
Whether the state is still in use. Inactive states are kept because historical appointments still point at them.
SortOrder
Int32
No
Position in the back-office list. The endpoint returns rows already ordered by this.
Colour
String
Yes
The state's diary colour as a hex string. Same value as AppointmentList.AppointmentStateColour.
SiteId
Guid
Yes
The site the state belongs to. Null on an account-wide state.
One referral source - how a client or a sale came to the business. Returned by GET /api/v2/sources.
This is what Sales.Source resolves against, but note the join is by name, not by id: the sales report carries the source's name as a string rather than its SourceId.
Fields of the SourceList object
Field
Type
Null
Description
SourceId
Guid
Yes
The source's id.
SiteId
Guid
Yes
The site the source belongs to. Null on an account-wide source.
State
Byte
No
Whether the source is active, as a number - the same item-state set the itemState query parameter uses elsewhere.
Code
String
Yes
The account's own short code for the source, at most 5 characters.
Name
String
Yes
Display name. The endpoint returns rows ordered by this.
CommissionPercentage
Decimal
No
Commission percentage attributed to the source, where the account pays referral commission.
The account as ClassPass sees it. Returned by GET /api/v2/classpass.
One ChiDesk account is exactly one ClassPass partner, so partners always holds a single entry and the pagination block is hard-coded to page 1 of 1 - the page argument is accepted and ignored. Do not write a paging loop against this endpoint. Note also that the partner's last_updated is generated as "now minus one day" on every call rather than being a real modification time, and partner_description is never populated.
Fields of the partner_list object
Field
Type
Null
Description
partners
partner[]
Yes
Always exactly one entry. Each partner carries partner_id (the ChiDesk account name), partner_name (the account's BusinessName setting), partner_description (never populated) and last_updated.
pagination
pagination
Yes
Always page 1, total_pages 1. Carries page, page_size and total_pages; page_size echoes what you sent.
One partner, wrapped. Returned by GET /api/v2/classpass/{partner_id}.
A wrapper object, not the partner itself - the partner is one level down, under the partner key. This is ClassPass's convention for single-item fetches and it applies to venue_item too.
Fields of the partner_item object
Field
Type
Null
Description
partner
partner
Yes
The partner: partner_id, partner_name, partner_description and last_updated.
The account's sites as ClassPass venues. Returned by GET /api/v2/classpass/{partner_id}/venues.
The pagination block here always reads page 1 of 1 regardless of how many venues come back, so it cannot be used to page. Read the whole venues array in one call.
Fields of the venue_list object
Field
Type
Null
Description
venues
venue[]
Yes
One entry per site. Each venue carries partner_id, venue_id, venue_name, venue_description, phone, email, website, a nested address and last_updated.
One venue, wrapped. Returned by GET /api/v2/classpass/{partner_id}/venues/{venue_id}.
Fields of the venue_item object
Field
Type
Null
Description
venue
venue
Yes
The venue: partner_id, venue_id, venue_name, venue_description, phone, email, website, address and last_updated. The nested address carries address_line1, address_line2, city, state, zip and country - a different shape from ChiDesk's own Address object.
The services bookable at a venue. Returned by GET /api/v2/classpass/{partner_id}/venues/{venue_id}/services.
Unlike the partner and venue lists, this endpoint pages for real - total_pages is computed from the result count and the array is sliced to the page you asked for. Loop until page reaches total_pages.
Fields of the service_list object
Field
Type
Null
Description
services
service[]
Yes
The page of services. Each carries service_id, name, duration, start_interval (how far apart bookable start times are, in minutes), description, late_cancel_window and last_updated. Note duration and late_cancel_window are strings, not numbers.
pagination
pagination
Yes
A real pagination block: page, page_size and a total_pages computed from the full result count.
The practitioners who can be booked at a venue. Returned by GET /api/v2/classpass/{partner_id}/venues/{venue_id}/bookables.
Bookables are employees only. Rooms and equipment are not offered through this integration, so a service that needs a resource still resolves to the staff member here. This endpoint pages for real.
Fields of the bookable_list object
Field
Type
Null
Description
bookables
bookable[]
Yes
The page of practitioners. Each carries type, bookable_id, first_name, last_name, gender, display_name, biography, last_updated and a services array of the service_ids they can deliver.
pagination
pagination
Yes
A real pagination block: page, page_size and a computed total_pages.
When each practitioner is free, over a requested date range. Returned by GET /api/v2/classpass/{partner_id}/venues/{venue_id}/availability.
A practitioner with no free time in the range is omitted from the array entirely rather than returned with an empty availability_windows list - absence means no availability, not an error.
Fields of the availability_list object
Field
Type
Null
Description
availabilities
availability[]
Yes
One entry per practitioner who has free time. Each carries type, bookable_id, last_updated and an availability_windows array, where every window is a start_datetime and end_datetime pair.
A booking coming in from ClassPass. The body POST /api/v2/classpass/{partner_id}/venues/{venue_id}/appointments accepts.
This is ClassPass's appointment shape and has nothing in common with ChiDesk's own Appointment object - different casing, different fields, and the id is a string rather than a Guid because ClassPass assigns it. The response echoes only { "appointment_id": "..." }, not the object you sent.
Fields of the appointment object
Field
Type
Null
Description
appointment_id
String
Yes
ClassPass's id for the booking. You supply this - ChiDesk stores it against the appointment it creates and echoes it back.
bookables
bookable[]
Yes
Which practitioner the booking is with. Only bookable_id is read; the rest of the bookable shape may be sent but is ignored.
service_id
String
Yes
The service being booked, as returned by the services endpoint.
start_datetime
String
Yes
When the booking starts, as a string rather than a parsed date - the same format the availability windows use.
customization
String
Yes
Free-text customisation passed through from ClassPass.
user
user
Yes
The client. Carries user_id, user_email, user_username, first_name, last_name, gender, phone, birthday, a nested address and an emergency_contact of name and phone. ChiDesk matches or creates a client record from this.
A status change on an existing ClassPass booking. The body PATCH /api/v2/classpass/{partner_id}/venues/{venue_id}/appointments/{appointment_id} accepts.
Only CANCELLED and LATE CANCELLED do anything. Any other status returns 200 with the appointment unchanged, so a successful response is not evidence that the status was applied - check the appointment afterwards if it matters.
Fields of the appointment_status object
Field
Type
Null
Description
status
String
Yes
The new status. Only CANCELLED and LATE CANCELLED have any effect.
customization
String
Yes
Free-text customisation passed through from ClassPass.
Whether the client actually turned up. Returned by GET /api/v2/classpass/{partner_id}/venues/{venue_id}/appointments/{appointment_id}/attendance.
The array holds exactly one record, for the appointment you asked about. Its status is either ATTENDED or CANCELLED - there is no third value and no "unknown", so a booking that was never marked off reads as one of the two.
Fields of the attendance_list object
Field
Type
Null
Description
attendance
attendance[]
Yes
Exactly one record, carrying cp_user_id, appointment_id, status and last_updated.
A postal address, carried inside larger objects rather than returned on its own.
Cities, States and Countries are picker lists for the back-office form, not data about this address. They are serialized because the type is shared with the admin UI, and they are normally null on an API response - do not read them as the address's values.
Fields of the Address object
Field
Type
Null
Description
AddressId
Guid
No
The address's id.
Street
String
Yes
Street address.
City
String
Yes
City.
State
String
Yes
Province or state - the address field, not a status flag.
Code
String
Yes
Postal code.
Country
String
Yes
Country. Required when writing an address.
Cities
String[]
Yes
Picker list, not this address's value. See the note above.
States
String[]
Yes
Picker list, not this address's value.
Countries
String[]
Yes
Picker list, not this address's value.
Nothing matched
Search looks at the method, the path, the summary and the parameter names of every documented
endpoint. Try a shorter term - site rather than siteId - or a method
name such as post.
Search covers every endpoint in every category, and every object schema - so a field name
such as SellInclusive will find the objects that carry it, not just the
endpoints that return them.