RoundPro API
Connect a CRM like GoHighLevel, automations in Zapier, an AI assistant or your own software to a window cleaning business’s RoundPro account. Look up customers and the schedule; add and update customers, create and change jobs, and move or reassign cleans; and be told the moment something happens, with webhooks.
- Base address
- https://roundpro.app/api/v1
- Sign in with
- Authorization: Bearer rp_live_…
- Format
- JSON · money in pence · dates YYYY-MM-DD
- Version
- 1
Webhooks: being told when things happen
Instead of asking RoundPro over and over whether anything has changed, a system can be told the moment something happens. The business owner adds the address to send to in Settings → API & integrations → Add a webhook, and ticks the events wanted (or everything).
| Event | Sent when | data is a |
|---|
| customer.created | A new customer is added (in RoundPro, through the API, or by accepting a quote). | Customer |
| customer.updated | A customer's details, notes or one of their properties is changed. | Customer |
| customer.archived | A customer is archived (they've stopped). Their jobs are stopped too. | Customer |
| customer.restored | An archived customer is brought back. | Customer |
| job.created | New regular or one-off work is set up at a property. | Job |
| job.updated | A job's price, frequency, round, worker, start date, months off or details change. | Job |
| job.stopped | A job is stopped and its upcoming cleans removed. | Job |
| job.restarted | A stopped job is started again. | Job |
| visit.completed | A clean is marked done. | Visit |
| visit.skipped | A clean is skipped, with the reason. | Visit |
| visit.moved | A clean is moved to another day. previous.date says where it was. | Visit |
| invoice.sent | An invoice goes to the customer for the first time. | Invoice |
| invoice.paid | An invoice is paid in full. | Invoice |
| payment.received | Money is recorded from a customer (any method). | Payment |
| lead.created | An enquiry comes in (through the API or Zapier), after RoundPro has tried to price it: status says whether a quote went. | Lead |
| quote.sent | A quote goes to someone for the first time (by hand, or automatically from a lead). | Quote |
| quote.declined | Someone says no thanks to a quote, with anything they wrote. | Quote |
| quote.accepted | A quote is accepted (online, or converted by hand): the customer and their new job. | { quoteId, customer, job } |
Each event is a POST of JSON to the address. data is the record as the API returns it (so the same fields as GET /customers/{id} and so on), as it was when the message was first sent.
POST https://your-system.example.com/roundpro
Content-Type: application/json
RoundPro-Event: visit.moved
RoundPro-Event-Id: muxw3pteaClPBAhsp2Vj
RoundPro-Delivery-Id: muxw3q8kR2mXbN0aLp4Q
RoundPro-Signature: t=1791367200,v1=5f2b…
{
"id": "muxw3pteaClPBAhsp2Vj",
"type": "visit.moved",
"createdAt": "2026-10-07T09:20:00.000Z",
"business": { "id": "mumqddmwmmmSpnzVV2OQ", "name": "Crystal Clear Windows" },
"source": { "kind": "api", "keyId": "…", "keyName": "GoHighLevel" },
"data": { "id": "vis…", "jobId": "job…", "customerId": "cus…", "date": "2026-10-16", "originalDate": "2026-10-14", "status": "scheduled", … },
"previous": { "date": "2026-10-14" }
}
- Answer with any 2xx status within 10 seconds to say it arrived. Do the slow work afterwards.
- Anything else (an error, no answer, a redirect) is tried again after 1, 5, 15 minutes, then 1, 3, 6, 14 hours — 8 tries over about a day. An address still failing after 24 hours is switched off and the owner emailed.
- The same event can occasionally arrive twice (for example if your answer was lost). Use id to ignore one you've already handled.
- Events can arrive out of order. Use createdAt, or look the record up with the API if order matters.
- source.kind says what caused it: app (someone in RoundPro), api (with keyId and keyName), customer (on their own page, or accepting a quote online) or system. If your system both changes RoundPro through the API and listens for events, ignore events whose source.keyId is your own key, so changes don't bounce back and forth.
- Events while a webhook is switched off aren't sent later. Imports and restoring a backup don't send events.
- Only https addresses on the public internet are used, on the normal port.
Checking a message really came from RoundPro: each webhook has a signing secret (shown once, when it's added). The RoundPro-Signature header has a time (t) and an HMAC-SHA256 (v1) of the time, a full stop, and the body exactly as sent, made with that secret. Work out the same and compare; refuse messages more than 5 minutes old. Zapier and GoHighLevel don't need this, but your own software should do it.
import crypto from "node:crypto";
// rawBody: the request body exactly as received (before JSON parsing)
function isFromRoundPro(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=")));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!parts.t || !parts.v1 || age > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "hex"), b = Buffer.from(parts.v1, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Send a test from the webhook's page in Settings to check it's working: the event type is test.ping.
A system can also subscribe for itself with its API key (POST /webhooks, and DELETE /webhooks/{id} to stop) — this is how Zapier's instant triggers work. Those subscriptions show in the owner's Settings marked as added through the API, and go if the key is revoked.
Examples
Add a new customer and book them in every 4 weeks on the Monday round:
POST https://roundpro.app/api/v1/customers
{ "name": "Sarah Jones", "email": "sarah@example.com", "phone": "07700 900123",
"property": { "address1": "10 Holyhead Road", "town": "Telford", "postcode": "TF1 1AJ", "accessNotes": "Side gate, code 1234" } }
→ 201 { "id": "cus…", "properties": [ { "id": "prop…", … } ], … }
GET https://roundpro.app/api/v1/rounds
→ { "data": [ { "id": "rnd…", "name": "Monday" }, … ] }
POST https://roundpro.app/api/v1/jobs
{ "propertyId": "prop…", "pricePence": 1800, "frequencyWeeks": 4, "startDate": "2026-10-12", "roundId": "rnd…" }
→ 201 { "id": "job…", "nextVisitDate": "2026-10-12", … }
Find a customer by phone number or postcode:
GET https://roundpro.app/api/v1/customers?search=07700900123
GET https://roundpro.app/api/v1/customers?search=TF1%201AJ
Change a job to every 6 weeks and move it to another round:
PATCH https://roundpro.app/api/v1/jobs/job…
{ "frequencyWeeks": 6, "roundId": "rnd…" }
See tomorrow's work, then move one clean to Friday and give it to Dan:
GET https://roundpro.app/api/v1/visits?from=2026-10-08&to=2026-10-08
GET https://roundpro.app/api/v1/team
PATCH https://roundpro.app/api/v1/visits/vis…
{ "date": "2026-10-10", "operativeId": "usr…" }
Add a note to a customer (it's dated and marked with your key's name):
POST https://roundpro.app/api/v1/customers/cus…/notes
{ "text": "Asked for the conservatory roof next time" }
Customers
The people a business cleans for. Each has one or more properties.
GET/customers
List or search customers
Customers with their properties, newest first. Use search to find someone by name, email, phone, address or postcode. Archived customers are left out unless you ask for them.
Permission: Read (read)
Query parameters
| Field | Type | Notes |
|---|
| limitUp to 100 (default 50). 1–100. Default 50 | integer | Up to 100 (default 50). 1–100. Default 50 |
| cursorUp to 200 characters | string | Up to 200 characters |
| searchMatches name, email, phone, address or postcode. Up to 100 characters | string | Matches name, email, phone, address or postcode. Up to 100 characters |
| archivedOne of: "false", "true", "all". Default "false" | string | One of: "false", "true", "all". Default "false" |
Answers with a page of Customer objects (data, nextCursor).
curl "https://roundpro.app/api/v1/customers?search=Jones" \
-H "Authorization: Bearer $ROUNDPRO_KEY"
GET/customers/{id}
Get a customer
One customer with their properties.
Permission: Read (read)
Answers with the Customer.
curl "https://roundpro.app/api/v1/customers/CUSTOMER_ID" \
-H "Authorization: Bearer $ROUNDPRO_KEY"
POST/customers
Add a customer
Adds a customer, and their first property if you give the address. Add a job afterwards (POST /jobs) to put them on the schedule. Nothing is sent to the customer.
Permission: Customers (customers:write) · Answers 201 Created
Body
| Field | Type | Notes |
|---|
| namerequiredUp to 120 characters | string | Up to 120 characters |
| email | string or null | |
| phoneUp to 30 characters | string or null | Up to 30 characters |
| notesUp to 5000 characters | string or null | Up to 5000 characters |
| sourceWhere they came from, e.g. "Facebook – Spring offer". Up to 80 characters | string or null | Where they came from, e.g. "Facebook – Spring offer". Up to 80 characters |
| contactByHow invoices and reminders reach them. One of: "email", "text" | string or null | How invoices and reminders reach them. One of: "email", "text" |
| emailOptIn | boolean | |
| textOptIn | boolean | |
| property | object | |
| property.address1requiredUp to 120 characters | string | Up to 120 characters |
| property.address2Up to 120 characters | string or null | Up to 120 characters |
| property.townUp to 80 characters | string or null | Up to 80 characters |
| property.postcodeUp to 10 characters | string or null | Up to 10 characters |
| property.accessNotesUp to 1000 characters | string or null | Up to 1000 characters |
| property.what3wordsThree words with dots, like filled.count.soap. Up to 80 characters | string or null | Three words with dots, like filled.count.soap. Up to 80 characters |
Answers with the Customer.
curl -X POST "https://roundpro.app/api/v1/customers" \
-H "Authorization: Bearer $ROUNDPRO_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"name":"Sarah Jones","email":"sarah@example.com","phone":"07700 900123","property":{"address1":"10 Holyhead Road","town":"Telford","postcode":"TF1 1AJ","accessNotes":"Side gate, code 1234"}}'
PATCH/customers/{id}
Update a customer
Change any of the fields given; the rest stay as they are. Sending notes replaces the notes — to add a line instead, use POST /customers/{id}/notes.
Permission: Customers (customers:write)
Body
| Field | Type | Notes |
|---|
| nameUp to 120 characters | string | Up to 120 characters |
| email | string or null | |
| phoneUp to 30 characters | string or null | Up to 30 characters |
| notesUp to 5000 characters | string or null | Up to 5000 characters |
| contactByHow invoices and reminders reach them. One of: "email", "text" | string or null | How invoices and reminders reach them. One of: "email", "text" |
| emailOptIn | boolean | |
| textOptIn | boolean | |
| messagesPaused | boolean | |
| sourceWhere they came from, e.g. "Facebook – Spring offer". Up to 80 characters | string or null | Where they came from, e.g. "Facebook – Spring offer". Up to 80 characters |
Answers with the Customer.
curl -X PATCH "https://roundpro.app/api/v1/customers/CUSTOMER_ID" \
-H "Authorization: Bearer $ROUNDPRO_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"phone":"07700 900456","contactBy":"text"}'
POST/customers/{id}/notes
Add a note to a customer
Adds a dated line to the end of the customer's notes, marked as coming from this key.
Permission: Customers (customers:write)
Body
| Field | Type | Notes |
|---|
| textrequiredUp to 1000 characters | string | Up to 1000 characters |
Answers with the Customer.
curl -X POST "https://roundpro.app/api/v1/customers/CUSTOMER_ID/notes" \
-H "Authorization: Bearer $ROUNDPRO_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"text":"Asked for the conservatory roof next time"}'
POST/customers/{id}/archive
Archive a customer
For a customer who's stopped: their jobs stop and their upcoming cleans are removed. Their history and invoices are kept, and they can be restored. (Customers are never deleted through the API.)
Permission: Customers (customers:write)
Body
| Field | Type | Notes |
|---|
| reasonWhy they left. One of: "MOVED", "PRICE", "QUALITY", "OTHER_CLEANER", "NOT_NEEDED", "WE_STOPPED", "UNKNOWN" | string | Why they left. One of: "MOVED", "PRICE", "QUALITY", "OTHER_CLEANER", "NOT_NEEDED", "WE_STOPPED", "UNKNOWN" |
| noteUp to 300 characters | string or null | Up to 300 characters |
Answers with the Customer.
curl -X POST "https://roundpro.app/api/v1/customers/CUSTOMER_ID/archive" \
-H "Authorization: Bearer $ROUNDPRO_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"reason":"MOVED","note":"Moved to Shrewsbury"}'
POST/customers/{id}/restore
Restore an archived customer
Brings an archived customer back. Their jobs stay stopped — restart the ones you need (POST /jobs/{id}/restart).
Permission: Customers (customers:write)
Answers with the Customer.
curl -X POST "https://roundpro.app/api/v1/customers/CUSTOMER_ID/restore" \
-H "Authorization: Bearer $ROUNDPRO_KEY" \
-H "Idempotency-Key: $(uuidgen)"
Leads and quotes
Enquiries from people who aren't customers yet (from a Facebook lead form, a website and so on), and the quotes sent to them. A lead is priced from the business's price list; with automatic quoting on, the quote goes straight away.
POST/leads
Add a lead
Adds an enquiry, then deals with it as the business has set up in Settings › ALICE – Lead Assistant: it's priced from the price list and, with automatic quoting on, the quote is texted or emailed to them straight away. Otherwise it waits in Leads for the business (a thank-you may be sent). The answer says what happened (outcome). The same person enquiring again within 7 days is kept as a duplicate and nothing is sent twice. Give the answers as people typed them: "Semi-detached", "3 bedrooms", "Every 4 weeks" are all understood.
Permission: Leads (leads:write) · Answers 201 Created
Body
| Field | Type | Notes |
|---|
| namerequiredUp to 120 characters | string | Up to 120 characters |
| email | string or null | |
| phoneA UK mobile lets RoundPro text them. Up to 30 characters | string or null | A UK mobile lets RoundPro text them. Up to 30 characters |
| address1Needed before a quote can be made. Up to 120 characters | string or null | Needed before a quote can be made. Up to 120 characters |
| address2Up to 120 characters | string or null | Up to 120 characters |
| townUp to 80 characters | string or null | Up to 80 characters |
| postcodeUp to 10 characters | string or null | Up to 10 characters |
| propertyTypeAs they answered, e.g. "Semi-detached house" (flat, terraced, semi, detached, bungalow). Up to 60 characters | string or null | As they answered, e.g. "Semi-detached house" (flat, terraced, semi, detached, bungalow). Up to 60 characters |
| bedroomse.g. 3 or "3 bedrooms" | string or integer or null | e.g. 3 or "3 bedrooms" |
| extrasExtras they want, as a list or comma separated, e.g. "Conservatory, Extension" | string or array of string or null | Extras they want, as a list or comma separated, e.g. "Conservatory, Extension" |
| frequencyHow often, e.g. "Every 4 weeks", "Monthly", "One-off" (or a number of weeks) | string or integer or null | How often, e.g. "Every 4 weeks", "Monthly", "One-off" (or a number of weeks) |
| messageAnything else they wrote. Up to 2000 characters | string or null | Anything else they wrote. Up to 2000 characters |
| answersAny other questions and answers from the form (up to 20) | object | Any other questions and answers from the form (up to 20) |
| sourceWhere it came from, e.g. "Facebook". Up to 60 characters | string or null | Where it came from, e.g. "Facebook". Up to 60 characters |
| campaignThe ad, campaign or form, e.g. "Spring offer". Up to 120 characters | string or null | The ad, campaign or form, e.g. "Spring offer". Up to 120 characters |
Answers with the Lead.
curl -X POST "https://roundpro.app/api/v1/leads" \
-H "Authorization: Bearer $ROUNDPRO_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"name":"Sarah Jones","email":"sarah@example.com","phone":"07700 900123","address1":"10 Holyhead Road","town":"Telford","postcode":"TF1 1AJ","propertyType":"Semi-detached","bedrooms":"3","extras":"Conservatory","frequency":"Every 4 weeks","source":"Facebook","campaign":"Spring offer"}'
GET/leads
List leads
Leads, newest first.
Permission: Read (read)
Query parameters
| Field | Type | Notes |
|---|
| limitUp to 100 (default 50). 1–100. Default 50 | integer | Up to 100 (default 50). 1–100. Default 50 |
| cursorUp to 200 characters | string | Up to 200 characters |
| statusOne of: "new", "quoted", "closed" | string | One of: "new", "quoted", "closed" |
| sourceOnly leads from this source. Up to 60 characters | string | Only leads from this source. Up to 60 characters |
Answers with a page of Lead objects (data, nextCursor).
curl "https://roundpro.app/api/v1/leads" \
-H "Authorization: Bearer $ROUNDPRO_KEY"
GET/leads/{id}
Get a lead
One lead, with what happened to it.
Permission: Read (read)
Answers with the Lead.
curl "https://roundpro.app/api/v1/leads/ID" \
-H "Authorization: Bearer $ROUNDPRO_KEY"
GET/quotes
List quotes
Quotes, newest first. Filter by status, or by the lead or customer they belong to.
Permission: Read (read)
Query parameters
| Field | Type | Notes |
|---|
| limitUp to 100 (default 50). 1–100. Default 50 | integer | Up to 100 (default 50). 1–100. Default 50 |
| cursorUp to 200 characters | string | Up to 200 characters |
| statusOne of: "draft", "sent", "viewed", "accepted", "declined", "expired", "withdrawn" | string | One of: "draft", "sent", "viewed", "accepted", "declined", "expired", "withdrawn" |
| leadIdAn ID, as returned by the API. Up to 64 characters | string | An ID, as returned by the API. Up to 64 characters |
| customerIdAn ID, as returned by the API. Up to 64 characters | string | An ID, as returned by the API. Up to 64 characters |
Answers with a page of Quote objects (data, nextCursor).
curl "https://roundpro.app/api/v1/quotes" \
-H "Authorization: Bearer $ROUNDPRO_KEY"
GET/quotes/{id}
Get a quote
One quote: what was offered, and whether it's been sent, opened and answered.
Permission: Read (read)
Answers with the Quote.
curl "https://roundpro.app/api/v1/quotes/ID" \
-H "Authorization: Bearer $ROUNDPRO_KEY"
POST/quotes
Make a quote
A quote for an existing customer (customerId, and optionally one of their properties) or someone new (name, address and an email or UK mobile). The price is the total of the services. With send: true it's emailed or texted to them straight away with a link to accept; otherwise it's kept as a draft for the owner to check and send. When they accept, they become a customer with a job.
Permission: Quotes and replies (quotes:write) · Answers 201 Created
Body
| Field | Type | Notes |
|---|
| customerIdQuote an existing customer (their details are used). Up to 64 characters | string | Quote an existing customer (their details are used). Up to 64 characters |
| propertyIdOne of that customer's properties; leave out to use their first, or give address1 for a new address. Up to 64 characters | string | One of that customer's properties; leave out to use their first, or give address1 for a new address. Up to 64 characters |
| nameFor someone who isn't a customer yet. Up to 120 characters | string | For someone who isn't a customer yet. Up to 120 characters |
| email | string or null | |
| phoneA UK mobile, to send it by text. Up to 30 characters | string or null | A UK mobile, to send it by text. Up to 30 characters |
| address1Up to 120 characters | string or null | Up to 120 characters |
| address2Up to 120 characters | string or null | Up to 120 characters |
| townUp to 80 characters | string or null | Up to 80 characters |
| postcodeUp to 10 characters | string or null | Up to 10 characters |
| servicesrequiredWhat's being quoted; the price is their total. Up to 10 | array of object | What's being quoted; the price is their total. Up to 10 |
| services[].namerequiredWhat it is, e.g. "Roof clean" or "Windows front & back". Up to 120 characters | string | What it is, e.g. "Roof clean" or "Windows front & back". Up to 120 characters |
| services[].pricePencerequiredWhole pence (£25.50 is 2550) | integer | Whole pence (£25.50 is 2550) |
| frequencyWeeksrequiredHow often (weeks), or null for a one-off. How many weeks between cleans. 1–52 | integer or null | How often (weeks), or null for a one-off. How many weeks between cleans. 1–52 |
| contactByHow to send it (default: email if there's an address, else text). One of: "email", "text" | string | How to send it (default: email if there's an address, else text). One of: "email", "text" |
| noteA note shown on the quote. Up to 1000 characters | string or null | A note shown on the quote. Up to 1000 characters |
| sendSend it straight away (true), or keep it as a draft for the owner to check (false). Default false | boolean | Send it straight away (true), or keep it as a draft for the owner to check (false). Default false |
Answers with the Quote.
curl -X POST "https://roundpro.app/api/v1/quotes" \
-H "Authorization: Bearer $ROUNDPRO_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"customerId":"cus_123","services":[{"name":"Roof clean","pricePence":100000}],"frequencyWeeks":null,"send":false}'
POST/quotes/{id}/send
Send a quote
Emails or texts the quote to them with a link to view and accept (sending again sends a fresh link). Refused if it's already been answered or withdrawn. This messages the customer and can't be undone.
Permission: Quotes and replies (quotes:write)
Body
| Field | Type | Notes |
|---|
| contactBySend by email or text (default: as set on the quote). One of: "email", "text" | string | Send by email or text (default: as set on the quote). One of: "email", "text" |
Answers with the Quote.
curl -X POST "https://roundpro.app/api/v1/quotes/ID/send" \
-H "Authorization: Bearer $ROUNDPRO_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{}'
POST/leads/{id}/reply
Reply to a lead
Sends a message to the lead in the conversation they started (Facebook Messenger or email), as the business. The AI assistant stands back from that conversation afterwards. This messages them and can't be undone.
Permission: Quotes and replies (quotes:write)
Body
| Field | Type | Notes |
|---|
| messagerequiredWhat to say to them, in the conversation they started (Messenger or email). Up to 1000 characters | string | What to say to them, in the conversation they started (Messenger or email). Up to 1000 characters |
Answers with the Lead.
curl -X POST "https://roundpro.app/api/v1/leads/ID/reply" \
-H "Authorization: Bearer $ROUNDPRO_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{}'