REST API reference
ZenMaid API
13 endpoints for ZenMaid. Each one is a POST with a JSON body, called with your API key.
- Base URL
- https://cotlet.dev/api/v1/zenmaid
- Connects with
- Your ZenMaid owner or office manager account
- Integration page
- ZenMaid
| Endpoint | Access | Description |
|---|---|---|
| list_appointments | read | Appointments in a date range with customer, time, cleaners, price and paid flag. |
| get_appointment | read | Full appointment: address, service type, frequency, work items, billing, notes and cleaners. |
| list_contacts | read | Contacts with email, phone, address and status. |
| get_contact | read | One contact with notes, custom fields, and past and upcoming appointments. |
| list_cleaners | read | Active cleaners and teams with their IDs. |
| create_contact | write | Add a new contact. ZenMaid starts it as a lead. |
| create_appointment | write | Schedule a one-time or recurring appointment for a contact. |
| reschedule_appointment | write | Move one appointment to a new date or time. |
| assign_cleaners | write | Set the cleaners or team on one appointment, replacing the current ones. |
| add_note | write | Add a note to an appointment or to a contact. |
| cancel_appointment | write | Set an appointment to Cancelled, optionally notifying the client and cleaner. |
| mark_appointment_paid | write | Record an offline payment such as cash, Venmo or PayPal. Does not charge a card. |
| send_message | write | Send a customer an email, text or both from one of your communication templates. |
List appointments
readPOST https://cotlet.dev/api/v1/zenmaid/list_appointments
Returns appointments between two dates, in date and time order. An appointment with nobody assigned has an empty cleaners list. Filter by payment status to find unpaid jobs, or search by customer name, cleaner name, street address or a custom field, as ZenMaid's List View does. Cancelled appointments are included only when you ask for them.
Parameters
| Name | Type | Description |
|---|---|---|
| from required | date | First day of the range. |
| to required | date | Last day of the range. |
| query | string | Customer name, cleaner name, street address or custom field value. |
| paid | enum | all, paid or unpaid. Default: all. |
| include_cancelled | boolean | Also return cancelled appointments. Default: false. |
| limit | integer | 1 to 100. Default: 50. |
| cursor | string | Next page cursor from the previous call. |
Request
curl -X POST https://cotlet.dev/api/v1/zenmaid/list_appointments \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"from":"2026-10-01","to":"2026-10-01"}' result = client.call("zenmaid", "list_appointments", {"from":"2026-10-01","to":"2026-10-01"}) const result = await cotlet.call('zenmaid', 'list_appointments', {"from":"2026-10-01","to":"2026-10-01"}); cotlet call zenmaid list_appointments --data '{"from":"2026-10-01","to":"2026-10-01"}' Example response
{
"appointments": [
{
"appointment_id": "a_993102",
"contact": "Sample Customer",
"date": "2026-10-16",
"start_time": "09:00",
"end_time": "12:00",
"cleaners": [],
"price": "180.00",
"paid": false
},
{
"appointment_id": "a_993140",
"contact": "Sample Household",
"date": "2026-10-16",
"start_time": "13:00",
"end_time": "15:00",
"cleaners": [
"Cleaner A"
],
"price": "120.00",
"paid": false
}
],
"next_cursor": null
} Get appointment
readPOST https://cotlet.dev/api/v1/zenmaid/get_appointment
Returns one appointment with the service address, service type, whether it belongs to a recurring service, work items, billing (price before tax, tip, paid flag) and its appointment and customer notes. Use it before changing or cancelling an appointment.
Parameters
| Name | Type | Description |
|---|---|---|
| appointment_id required | string | Appointment ID from list_appointments. |
Request
curl -X POST https://cotlet.dev/api/v1/zenmaid/get_appointment \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"appointment_id":"appointment_8c41d2"}' result = client.call("zenmaid", "get_appointment", {"appointment_id":"appointment_8c41d2"}) const result = await cotlet.call('zenmaid', 'get_appointment', {"appointment_id":"appointment_8c41d2"}); cotlet call zenmaid get_appointment --data '{"appointment_id":"appointment_8c41d2"}' Example response
{
"appointment_id": "a_993102",
"contact": {
"contact_id": "c_48213",
"name": "Sample Customer"
},
"address": "12 Sample Street, Springfield",
"date": "2026-10-16",
"start_time": "09:00",
"end_time": "12:00",
"service_type": "Deep Clean",
"recurring": false,
"cleaners": [],
"work_items": [
"Kitchen",
"Bathrooms"
],
"billing": {
"price": "180.00",
"tip": "0.00",
"paid": false
},
"notes": [
{
"visibility": "public",
"body": "Gate code is on the contact."
}
]
} List contacts
readPOST https://cotlet.dev/api/v1/zenmaid/list_contacts
Searches contacts by name, email, phone or address. Status is one of ZenMaid's four: lead, one_time_customer, recurring_customer or former_customer. ZenMaid sets it from the appointments on the calendar, so a new contact stays a lead until someone books them.
Parameters
| Name | Type | Description |
|---|---|---|
| query | string | Name, email, phone or address to search. |
| status | enum | lead, one_time_customer, recurring_customer or former_customer. |
| limit | integer | 1 to 100. Default: 50. |
| cursor | string | Next page cursor from the previous call. |
Request
curl -X POST https://cotlet.dev/api/v1/zenmaid/list_contacts \
-H "Authorization: Bearer $COTLET_API_KEY" result = client.call("zenmaid", "list_contacts") const result = await cotlet.call('zenmaid', 'list_contacts'); cotlet call zenmaid list_contacts Example response
{
"contacts": [
{
"contact_id": "c_48213",
"name": "Sample Customer",
"email": "customer@example.com",
"phone": "+1 555 0100",
"address": "12 Sample Street, Springfield",
"status": "lead"
}
],
"next_cursor": null
} Get contact
readPOST https://cotlet.dev/api/v1/zenmaid/get_contact
Returns one contact profile: addresses, customer notes (public and private), contact custom fields, revenue to date, unpaid balance and a list of their appointments, newest first. Private notes are returned only to owner and office manager accounts.
Parameters
| Name | Type | Description |
|---|---|---|
| contact_id required | string | Contact ID from list_contacts. |
Request
curl -X POST https://cotlet.dev/api/v1/zenmaid/get_contact \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_id":"contact_8c41d2"}' result = client.call("zenmaid", "get_contact", {"contact_id":"contact_8c41d2"}) const result = await cotlet.call('zenmaid', 'get_contact', {"contact_id":"contact_8c41d2"}); cotlet call zenmaid get_contact --data '{"contact_id":"contact_8c41d2"}' Example response
{
"contact_id": "c_48213",
"name": "Sample Customer",
"email": "customer@example.com",
"phone": "+1 555 0100",
"status": "one_time_customer",
"revenue_to_date": "180.00",
"unpaid_balance": "0.00",
"notes": [
{
"visibility": "private",
"body": "Prefers morning visits."
}
],
"appointments": [
{
"appointment_id": "a_993102",
"date": "2026-10-16",
"paid": false
}
]
} List cleaners
readPOST https://cotlet.dev/api/v1/zenmaid/list_cleaners
Returns the cleaners and teams in your account with their IDs, which assign_cleaners and create_appointment need. Inactive staff are left out unless you ask for them.
Parameters
| Name | Type | Description |
|---|---|---|
| include_inactive | boolean | Also list inactive staff. Default: false. |
Request
curl -X POST https://cotlet.dev/api/v1/zenmaid/list_cleaners \
-H "Authorization: Bearer $COTLET_API_KEY" result = client.call("zenmaid", "list_cleaners") const result = await cotlet.call('zenmaid', 'list_cleaners'); cotlet call zenmaid list_cleaners Example response
{
"cleaners": [
{
"cleaner_id": "cl_1207",
"name": "Cleaner A",
"type": "cleaner",
"active": true
},
{
"cleaner_id": "cl_1344",
"name": "Cleaner B",
"type": "cleaner",
"active": true
},
{
"cleaner_id": "tm_21",
"name": "Team One",
"type": "team",
"active": true
}
]
} Create contact
writePOST https://cotlet.dev/api/v1/zenmaid/create_contact
Adds a contact profile. ZenMaid starts a new contact as a Lead until an appointment is booked. It reaches no one and is undone in one call, so it does not ask for your OK.
Parameters
| Name | Type | Description |
|---|---|---|
| first_name required | string | First name. |
| last_name | string | Last name. |
| string | Email address. | |
| phone | string | Phone number. |
| address | string | Service address. |
| notes | string | Customer note, public by default. |
Request
curl -X POST https://cotlet.dev/api/v1/zenmaid/create_contact \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"first_name":"first name"}' result = client.call("zenmaid", "create_contact", {"first_name":"first name"}) const result = await cotlet.call('zenmaid', 'create_contact', {"first_name":"first name"}); cotlet call zenmaid create_contact --data '{"first_name":"first name"}' Example response
{
"contact_id": "c_48290",
"name": "Sample Lead",
"email": "lead@example.com",
"phone": "+1 555 0111",
"status": "lead"
} POST https://cotlet.dev/api/v1/zenmaid/create_appointment
Books an appointment for an existing contact with a start and end time. Leave cleaner_ids empty to keep it unassigned, and price empty to let ZenMaid calculate it if your account is set up for that. A recurring frequency creates a recurring service. The contact becomes a customer once the appointment is on the calendar. By default you confirm each call; you can switch that off for this tool.
Parameters
| Name | Type | Description |
|---|---|---|
| contact_id required | string | Contact ID from list_contacts. |
| date required | date | Day of the appointment. |
| start_time required | string | Start time, HH:MM. |
| end_time required | string | End time, HH:MM. |
| service_type | string | Service type as set up in your account. |
| frequency | string | one_time or a recurring frequency from your account. Default: one_time. |
| cleaner_ids | string[] | Cleaner or team IDs from list_cleaners. Empty leaves it unassigned. |
| price | string | Price before tax. Empty lets ZenMaid calculate it if configured. |
| notes | string | Appointment note. |
Request
curl -X POST https://cotlet.dev/api/v1/zenmaid/create_appointment \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contact_id":"c_48213","date":"2026-10-16","start_time":"09:00","end_time":"12:00","service_type":"Deep Clean","frequency":"one_time","cleaner_ids":["cl_1207"]}' result = client.call("zenmaid", "create_appointment", {"contact_id":"c_48213","date":"2026-10-16","start_time":"09:00","end_time":"12:00","service_type":"Deep Clean","frequency":"one_time","cleaner_ids":["cl_1207"]}) const result = await cotlet.call('zenmaid', 'create_appointment', {"contact_id":"c_48213","date":"2026-10-16","start_time":"09:00","end_time":"12:00","service_type":"Deep Clean","frequency":"one_time","cleaner_ids":["cl_1207"]}); cotlet call zenmaid create_appointment --data '{"contact_id":"c_48213","date":"2026-10-16","start_time":"09:00","end_time":"12:00","service_type":"Deep Clean","frequency":"one_time","cleaner_ids":["cl_1207"]}' Example response
{
"appointment_id": "a_993102",
"contact": "Sample Customer",
"date": "2026-10-16",
"start_time": "09:00",
"end_time": "12:00",
"service_type": "Deep Clean",
"cleaners": [
"Cleaner A"
],
"price": "180.00",
"paid": false
} POST https://cotlet.dev/api/v1/zenmaid/reschedule_appointment
Moves one appointment, as the Reschedule window does in ZenMaid. It keeps work items, price and status. The length stays the same unless you pass end_time. One-time appointment notes do not carry over, so add them again with add_note. For a recurring appointment, apply_to chooses this one or all future appointments. By default you confirm each call; you can switch that off for this tool.
Parameters
| Name | Type | Description |
|---|---|---|
| appointment_id required | string | Appointment ID from list_appointments. |
| date required | date | New day. |
| start_time required | string | New start time, HH:MM. |
| end_time | string | New end time, HH:MM. Default: keeps the length. |
| recalculate_price | boolean | Recalculate the amount owed for the new length. Default: false. |
| apply_to | enum | this_appointment or all_future, for recurring appointments. Default: this_appointment. |
Request
curl -X POST https://cotlet.dev/api/v1/zenmaid/reschedule_appointment \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"appointment_id":"a_993102","date":"2026-10-20","start_time":"09:00","apply_to":"this_appointment"}' result = client.call("zenmaid", "reschedule_appointment", {"appointment_id":"a_993102","date":"2026-10-20","start_time":"09:00","apply_to":"this_appointment"}) const result = await cotlet.call('zenmaid', 'reschedule_appointment', {"appointment_id":"a_993102","date":"2026-10-20","start_time":"09:00","apply_to":"this_appointment"}); cotlet call zenmaid reschedule_appointment --data '{"appointment_id":"a_993102","date":"2026-10-20","start_time":"09:00","apply_to":"this_appointment"}' Example response
{
"appointment_id": "a_993102",
"date": "2026-10-19",
"start_time": "10:00",
"end_time": "13:00",
"price": "180.00",
"price_recalculated": false
} Assign cleaners
writePOST https://cotlet.dev/api/v1/zenmaid/assign_cleaners
Replaces the cleaners on one appointment. It does not deactivate a cleaner and does not touch the default cleaner of a recurring service. One more call undoes it, so it does not ask for your OK.
Parameters
| Name | Type | Description |
|---|---|---|
| appointment_id required | string | Appointment ID from list_appointments. |
| cleaner_ids required | string[] | Cleaner or team IDs from list_cleaners. |
Request
curl -X POST https://cotlet.dev/api/v1/zenmaid/assign_cleaners \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"appointment_id":"a_993102","cleaner_ids":["cl_1344"]}' result = client.call("zenmaid", "assign_cleaners", {"appointment_id":"a_993102","cleaner_ids":["cl_1344"]}) const result = await cotlet.call('zenmaid', 'assign_cleaners', {"appointment_id":"a_993102","cleaner_ids":["cl_1344"]}); cotlet call zenmaid assign_cleaners --data '{"appointment_id":"a_993102","cleaner_ids":["cl_1344"]}' Example response
{
"appointment_id": "a_993102",
"cleaners": [
"Cleaner B"
]
} Add note
writePOST https://cotlet.dev/api/v1/zenmaid/add_note
Adds an Appointment Note or a Customer Note, as in ZenMaid. A public note goes on the work orders sent to cleaners; a private note is visible only to owners and office managers. It reaches no one and is undone by editing the note, so it does not ask for your OK.
Parameters
| Name | Type | Description |
|---|---|---|
| appointment_id | string | Appointment to note. Give this or contact_id. |
| contact_id | string | Contact to note. |
| body required | string | Note text. |
| visibility | enum | public or private. Default: public. |
Request
curl -X POST https://cotlet.dev/api/v1/zenmaid/add_note \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"body":"body"}' result = client.call("zenmaid", "add_note", {"body":"body"}) const result = await cotlet.call('zenmaid', 'add_note', {"body":"body"}); cotlet call zenmaid add_note --data '{"body":"body"}' Example response
{
"note_id": "n_5521",
"appointment_id": "a_993102",
"visibility": "public",
"body": "Moved one day because of rain."
} POST https://cotlet.dev/api/v1/zenmaid/cancel_appointment
Sets the status to Cancelled and keeps the record; it never deletes. notify takes none, client, cleaner or both, the same choices ZenMaid offers when you cancel. A cancelled appointment may be hidden by the calendar filters. By default you confirm each call; you can switch that off for this tool.
Parameters
| Name | Type | Description |
|---|---|---|
| appointment_id required | string | Appointment ID from list_appointments. |
| notify | enum | none, client, cleaner or both. Default: none. |
Request
curl -X POST https://cotlet.dev/api/v1/zenmaid/cancel_appointment \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"appointment_id":"appointment_8c41d2"}' result = client.call("zenmaid", "cancel_appointment", {"appointment_id":"appointment_8c41d2"}) const result = await cotlet.call('zenmaid', 'cancel_appointment', {"appointment_id":"appointment_8c41d2"}); cotlet call zenmaid cancel_appointment --data '{"appointment_id":"appointment_8c41d2"}' Example response
{
"appointment_id": "a_993102",
"status": "Cancelled",
"notified": [
"client",
"cleaner"
]
} POST https://cotlet.dev/api/v1/zenmaid/mark_appointment_paid
Marks one appointment as paid, like the Mark as paid button in its Billing section. It never charges the card on file. Appointments paid by card cannot be changed this way. The customer's Revenue To Date and Unpaid Balance update. By default you confirm each call; you can switch that off for this tool.
Parameters
| Name | Type | Description |
|---|---|---|
| appointment_id required | string | Appointment ID from list_appointments. |
Request
curl -X POST https://cotlet.dev/api/v1/zenmaid/mark_appointment_paid \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"appointment_id":"a_991870"}' result = client.call("zenmaid", "mark_appointment_paid", {"appointment_id":"a_991870"}) const result = await cotlet.call('zenmaid', 'mark_appointment_paid', {"appointment_id":"a_991870"}); cotlet call zenmaid mark_appointment_paid --data '{"appointment_id":"a_991870"}' Example response
{
"appointment_id": "a_991870",
"paid": true,
"total": "120.00"
} POST https://cotlet.dev/api/v1/zenmaid/send_message
Sends from a template under Type of communication, as the Send Text/Email action does. You can replace the template text with your own. ZenMaid only sends if the contact has an email address or phone number on file, and manual texts count toward your SMS usage. By default you confirm each call; you can switch that off for this tool.
Parameters
| Name | Type | Description |
|---|---|---|
| appointment_id | string | Appointment the message is about. Give this or contact_id. |
| contact_id | string | Contact to message. |
| template required | string | Communication template name. |
| channels | enum | email, sms or both. Default: email. |
| body | string | Replace the template text with your own. |
Request
curl -X POST https://cotlet.dev/api/v1/zenmaid/send_message \
-H "Authorization: Bearer $COTLET_API_KEY" \
-H "Content-Type: application/json" \
-d '{"template":"template"}' result = client.call("zenmaid", "send_message", {"template":"template"}) const result = await cotlet.call('zenmaid', 'send_message', {"template":"template"}); cotlet call zenmaid send_message --data '{"template":"template"}' Example response
{
"appointment_id": "a_993102",
"template": "Appointment Reminder",
"channels": [
"email",
"sms"
],
"sent": true
}