Site Builder
Services and Booking API
Your website uses three public endpoints: one to list services, one to load a single service and one to book it. Every request needs your API key in the x-provider-id header.
Base URL and headers
| Setting | Value |
|---|---|
| Base URL | https://api.getwecycle.com/api |
| Key header | x-provider-id: YOUR_SITE_KEY |
| Accept header | application/json |
| CORS | Open to every origin, so you can call it from any website |
The service list only returns services that belong to the business behind the key and are set to Active.
List services
/api/service?limit=100| Query | Default | Description |
|---|---|---|
limit | 10 | How many services to return |
skip | 0 | How many services to skip, for paging |
The response is an array of services:
[
{
"_id": "665f1c2e9b1d4a0012ab34cd",
"title": "Garage Door Repair",
"slug": "garage-door-repair",
"description": "Broken springs, cables and openers fixed fast.",
"cover": "https://res.cloudinary.com/.../cover.jpg",
"priceInputs": [],
"discount": null,
"isSpecificItemSupported": false
}
]
async function initializeServiceCatalog() {
const grid = document.querySelector("#service-grid");
const services = await request("/service?limit=100");
services.forEach((service) => {
const link = document.createElement("a");
link.href = `./checkout.html?serviceId=${encodeURIComponent(service._id)}`;
link.textContent = service.title;
grid.append(link);
});
grid.hidden = false;
}
Build cards with textContent instead of innerHTML. Service text is typed by people, so this keeps any stray HTML from running on your page.
Get one service
/api/service/:id?findby=id| Query | Values | Description |
|---|---|---|
findby | id, slug, title | How to read the :id part of the path |
Use findby=slug if you want clean links such as checkout.html?service=garage-door-repair.
Price inputs
A service can ask the customer extra questions that affect the price, like the number of doors. They come in priceInputs:
{
"name": "doors",
"label": "Number of doors",
"inputType": "number",
"required": true,
"options": []
}
| Field | Description |
|---|---|
name | Send this back with the answer |
label | Text to show next to the field |
inputType | number, text, select or radio |
required | Whether the customer must answer |
options | For select and radio: a list of label and value pairs |
Book a service
/api/service/:id/bookSend the booking as multipart/form-data. Build it with FormData and let the browser set the content type. Object fields are JSON strings.
| Field | Required | Content |
|---|---|---|
date | Yes | JSON: date (YYYY-MM-DD), mode (ONE_TIME), timeslot, timezone |
location | Yes | JSON: name, street, city, state, postalCode |
guestUser | Yes | JSON: firstName, lastName, email, phone, textConsent |
priceInputs | No | JSON array of name and value pairs |
isSpecificItem | No | "false" for a regular service booking |
specialNote | No | Notes from the customer |
idempotencyKey | Recommended | A unique ID per booking attempt, so a retry never creates a second job |
images | No | One or more photo files |
attribution | No | JSON with marketing data such as utmSource, utmCampaign, gclid, referrer and landingPage |
Timeslots are ALL_DAY, MORNING, AFTERNOON and EVENING. The timezone is the customer's IANA timezone, for example America/New_York.
function buildBookingPayload(values, idempotencyKey) {
const payload = new FormData();
payload.append(
"date",
JSON.stringify({
date: values.serviceDate,
mode: "ONE_TIME",
timeslot: values.timeslot,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
})
);
payload.append(
"location",
JSON.stringify({
name: `${values.street}, ${values.city}, ${values.state} ${values.postalCode}`,
street: values.street,
city: values.city,
state: values.state,
postalCode: values.postalCode,
})
);
payload.append(
"guestUser",
JSON.stringify({
firstName: values.firstName,
lastName: values.lastName,
email: values.email,
phone: values.phone,
textConsent: values.textConsent,
})
);
payload.append("priceInputs", JSON.stringify(values.priceInputs));
payload.append("isSpecificItem", "false");
payload.append("specialNote", values.specialNote);
payload.append("idempotencyKey", idempotencyKey);
return payload;
}
function createBooking(serviceId, payload) {
return request(`/service/${encodeURIComponent(serviceId)}/book`, {
method: "POST",
body: payload,
});
}
A successful booking returns the booking reference:
{ "bookingId": "66a0c7f19b1d4a0012ab9f31" }
The booking appears in your dashboard as a new job. No payment is taken at this step, so your team can review the job and confirm the visit with the customer.
Retries
Create the idempotencyKey once with crypto.randomUUID() and reuse it if the customer clicks the button again after an error. Clear it only after a successful booking.
Errors
Failed requests return a status code and a message. The message is usually a string, but validation errors can return an object with one message per field:
{ "message": "x-provider-id header is required" }
function formatApiError(payload, status) {
if (typeof payload?.message === "string" && payload.message.trim()) {
return payload.message;
}
if (payload?.message && typeof payload.message === "object") {
return Object.values(payload.message).filter(Boolean).join(" ");
}
return `The request could not be completed (${status}).`;
}
| Status | Meaning |
|---|---|
400 | Missing or invalid API key, or a required booking field is missing |
404 | No service with that ID, slug or title belongs to your business |
500 | Something went wrong on our side. Try again in a moment |
See Troubleshooting for common fixes.