Environment Setup
All examples use the Sandbox environment by default. Set the following environment variables:CLEANLIFE_API_KEY=pk_sandbox_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
CLEANLIFE_BASE_URL=https://apiv3.thecleanlife.dev/v1
CLEANLIFE_WEBHOOK_SECRET=a1b2c3d4...(64 hex chars)...
CLEANLIFE_BASE_URL to https://api.cleanlife.sa/v1 and use your production API key.
cURL
Find or Create Contact
curl -X POST "${CLEANLIFE_BASE_URL}/partners/contacts" \
-H "x-api-key: ${CLEANLIFE_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"phone": "+966500000000",
"name": "Ahmed Ali"
}'
Create a Booking
curl -X POST "${CLEANLIFE_BASE_URL}/partners/bookings" \
-H "x-api-key: ${CLEANLIFE_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"externalReference": "ORDER-20260615-001",
"contactId": "aaaaaaaa-0000-0000-0000-000000000001",
"addressId": "bbbbbbbb-0000-0000-0000-000000000002",
"serviceId": "dddddddd-0000-0000-0000-000000000004",
"hours": 3,
"date": "2026-06-20",
"timeslot": {
"startAt": "09:00",
"endAt": "12:00"
}
}'
Get Booking Status
curl -X GET "${CLEANLIFE_BASE_URL}/partners/bookings/11111111-0000-0000-0000-000000000001/status" \
-H "x-api-key: ${CLEANLIFE_API_KEY}"
Cancel a Booking
curl -X PATCH "${CLEANLIFE_BASE_URL}/partners/bookings/11111111-0000-0000-0000-000000000001/cancel" \
-H "x-api-key: ${CLEANLIFE_API_KEY}" \
-H "Content-Type: application/json" \
-d '{"notes": "Customer requested cancellation."}'
Create a Webhook Subscription
curl -X POST "${CLEANLIFE_BASE_URL}/partners/webhooks/subscriptions" \
-H "x-api-key: ${CLEANLIFE_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourplatform.com/webhooks/cleanlife",
"events": ["booking.created", "booking.cancelled", "booking.appointment_status_changed"]
}'
JavaScript (fetch)
const BASE_URL = process.env.CLEANLIFE_BASE_URL;
const API_KEY = process.env.CLEANLIFE_API_KEY;
const headers = {
'x-api-key': API_KEY,
'Content-Type': 'application/json',
};
// Find or Create Contact
async function findOrCreateContact({ phone, name, utmSource }) {
const response = await fetch(`${BASE_URL}/partners/contacts`, {
method: 'POST',
headers,
body: JSON.stringify({ phone, name, ...(utmSource && { utmSource }) }),
});
const data = await response.json();
if (!data.success) throw new Error(data.error.code + ': ' + data.error.message);
return data.data;
}
// Create Booking
async function createBooking(bookingData) {
const response = await fetch(`${BASE_URL}/partners/bookings`, {
method: 'POST',
headers,
body: JSON.stringify(bookingData),
});
const data = await response.json();
if (!data.success) throw new Error(data.error.code + ': ' + data.error.message);
return data.data;
}
// Get Booking Status
async function getBookingStatus(bookingId) {
const response = await fetch(`${BASE_URL}/partners/bookings/${bookingId}/status`, { headers });
const data = await response.json();
if (!data.success) throw new Error(data.error.code + ': ' + data.error.message);
return data.data;
}
// List Bookings
async function listBookings(page = 1, limit = 20, filters = {}) {
const params = new URLSearchParams({ page, limit, ...filters });
const response = await fetch(`${BASE_URL}/partners/bookings?${params}`, { headers });
const data = await response.json();
if (!data.success) throw new Error(data.error.code);
return data; // { data, meta }
}
// Verify Webhook Signature
const crypto = require('crypto');
function verifyWebhook(secret, rawBody, timestamp, signatureHeader) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
TypeScript (axios)
import axios, { AxiosInstance, AxiosError } from 'axios';
import * as crypto from 'crypto';
interface BookingResponse {
bookingId: string;
externalReference: string | null;
status: string;
paymentStatus: string;
trackingReference: string;
date: string;
timeslot: { startAt: string; endAt: string; endsAtNextDay: boolean };
appointment: null | { id: string; name: string; status: string; scheduledStartDateTime: string; scheduledEndDateTime: string };
}
interface PartnerApiSuccess<T> { success: true; data: T; }
interface PartnerApiError { success: false; error: { code: string; message: string; requestId: string }; }
class CleanLifePartnerClient {
private client: AxiosInstance;
constructor(apiKey: string, baseURL: string) {
this.client = axios.create({
baseURL,
timeout: 30_000,
headers: {
'x-api-key': apiKey,
'Content-Type': 'application/json',
},
});
}
async findOrCreateContact(data: {
phone: string;
name: string;
utmSource?: string;
}): Promise<{ contactId: string }> {
const response = await this.client.post<PartnerApiSuccess<{ contactId: string }>>('/partners/contacts', data);
return response.data.data;
}
async createBooking(data: {
externalReference?: string;
contactId: string;
addressId: string;
serviceId: string;
hours?: number;
date: string;
timeslot: { startAt: string; endAt: string };
}): Promise<BookingResponse> {
const response = await this.client.post<PartnerApiSuccess<BookingResponse>>('/partners/bookings', data);
return response.data.data;
}
async getBookingStatus(bookingId: string): Promise<BookingResponse> {
const response = await this.client.get<PartnerApiSuccess<BookingResponse>>(
`/partners/bookings/${bookingId}/status`
);
return response.data.data;
}
async cancelBooking(bookingId: string, options?: { reasonId?: string; notes?: string }): Promise<BookingResponse> {
const response = await this.client.patch<PartnerApiSuccess<BookingResponse>>(
`/partners/bookings/${bookingId}/cancel`,
options ?? {}
);
return response.data.data;
}
async confirmPayment(bookingId: string, paymentReference?: string): Promise<BookingResponse> {
const response = await this.client.post<PartnerApiSuccess<BookingResponse>>(
`/partners/bookings/${bookingId}/confirm-payment`,
paymentReference ? { paymentReference } : {}
);
return response.data.data;
}
static verifyWebhookSignature(
secret: string,
rawBody: string,
timestamp: string,
signatureHeader: string
): boolean {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
}
export { CleanLifePartnerClient };
Node.js (Express Webhook Handler)
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = process.env.CLEANLIFE_WEBHOOK_SECRET;
// IMPORTANT: Use express.raw() to get the raw body for signature verification
app.post('/webhooks/cleanlife', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['x-cleanos-signature'];
const timestamp = req.headers['x-cleanos-timestamp'];
const rawBody = req.body.toString('utf8');
// 1. Verify signature
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
console.warn('Invalid webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
const { event, deliveryId, payload } = JSON.parse(rawBody);
// 2. Respond immediately (before processing)
res.status(200).json({ received: true });
// 3. Process asynchronously
processWebhookAsync(event, deliveryId, payload).catch(console.error);
});
async function processWebhookAsync(event, deliveryId, payload) {
// Check for duplicates
const alreadyProcessed = await db.webhookLog.exists(deliveryId);
if (alreadyProcessed) {
console.log(`Duplicate webhook ignored: ${deliveryId}`);
return;
}
await db.webhookLog.record(deliveryId);
switch (event) {
case 'booking.created':
await handleBookingCreated(payload);
break;
case 'booking.cancelled':
await handleBookingCancelled(payload);
break;
case 'booking.appointment_status_changed':
await handleAppointmentStatusChanged(payload);
break;
case 'booking.payment_confirmed':
await handlePaymentConfirmed(payload);
break;
case 'booking.payment_failed':
await handlePaymentFailed(payload);
break;
default:
console.warn(`Unknown event type: ${event}`);
}
}
Python (requests)
import os
import hmac
import hashlib
import requests
from typing import Optional
class CleanLifePartnerClient:
def __init__(self, api_key: str, base_url: str):
self.base_url = base_url.rstrip('/')
self.session = requests.Session()
self.session.headers.update({
'x-api-key': api_key,
'Content-Type': 'application/json',
})
self.session.timeout = 30
def find_or_create_contact(self, phone: str, name: str, utm_source: Optional[str] = None) -> dict:
body = {'phone': phone, 'name': name}
if utm_source:
body['utmSource'] = utm_source
response = self.session.post(f'{self.base_url}/partners/contacts', json=body)
response.raise_for_status()
result = response.json()
if not result['success']:
raise Exception(f"{result['error']['code']}: {result['error']['message']}")
return result['data']
def create_booking(self, data: dict) -> dict:
response = self.session.post(f'{self.base_url}/partners/bookings', json=data)
response.raise_for_status()
result = response.json()
if not result['success']:
raise Exception(f"{result['error']['code']}: {result['error']['message']}")
return result['data']
def get_booking_status(self, booking_id: str) -> dict:
response = self.session.get(f'{self.base_url}/partners/bookings/{booking_id}/status')
response.raise_for_status()
result = response.json()
if not result['success']:
raise Exception(result['error']['code'])
return result['data']
def cancel_booking(self, booking_id: str, notes: Optional[str] = None) -> dict:
body = {}
if notes:
body['notes'] = notes
response = self.session.patch(f'{self.base_url}/partners/bookings/{booking_id}/cancel', json=body)
response.raise_for_status()
return response.json()['data']
def confirm_payment(self, booking_id: str, payment_reference: Optional[str] = None) -> dict:
body = {}
if payment_reference:
body['paymentReference'] = payment_reference
response = self.session.post(
f'{self.base_url}/partners/bookings/{booking_id}/confirm-payment',
json=body
)
response.raise_for_status()
return response.json()['data']
@staticmethod
def verify_webhook(secret: str, raw_body: str, timestamp: str, signature_header: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
f'{timestamp}.{raw_body}'.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)
# Usage
client = CleanLifePartnerClient(
api_key=os.environ['CLEANLIFE_API_KEY'],
base_url=os.environ['CLEANLIFE_BASE_URL']
)
booking = client.create_booking({
'externalReference': 'ORDER-20260615-001',
'contactId': 'aaaaaaaa-0000-0000-0000-000000000001',
'addressId': 'bbbbbbbb-0000-0000-0000-000000000002',
'serviceId': 'dddddddd-0000-0000-0000-000000000004',
'hours': 3,
'date': '2026-06-20',
'timeslot': {'startAt': '09:00', 'endAt': '12:00'}
})
print(booking['bookingId'])
C#
using System;
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
public class CleanLifePartnerClient
{
private readonly HttpClient _httpClient;
public CleanLifePartnerClient(string apiKey, string baseUrl)
{
_httpClient = new HttpClient { BaseAddress = new Uri(baseUrl), Timeout = TimeSpan.FromSeconds(30) };
_httpClient.DefaultRequestHeaders.Add("x-api-key", apiKey);
}
public async Task<JsonElement> CreateBookingAsync(object bookingData)
{
var response = await _httpClient.PostAsJsonAsync("/partners/bookings", bookingData);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
if (!result.GetProperty("success").GetBoolean())
{
var error = result.GetProperty("error");
throw new Exception($"{error.GetProperty("code").GetString()}: {error.GetProperty("message").GetString()}");
}
return result.GetProperty("data");
}
public async Task<JsonElement> GetBookingStatusAsync(string bookingId)
{
var response = await _httpClient.GetAsync($"/partners/bookings/{bookingId}/status");
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
return result.GetProperty("data");
}
public static bool VerifyWebhookSignature(string secret, string rawBody, string timestamp, string signatureHeader)
{
var key = Encoding.UTF8.GetBytes(secret);
var message = Encoding.UTF8.GetBytes($"{timestamp}.{rawBody}");
using var hmac = new HMACSHA256(key);
var hash = hmac.ComputeHash(message);
var expected = "sha256=" + BitConverter.ToString(hash).Replace("-", "").ToLower();
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(signatureHeader)
);
}
}
PHP
<?php
class CleanLifePartnerClient
{
private string $baseUrl;
private string $apiKey;
public function __construct(string $apiKey, string $baseUrl)
{
$this->apiKey = $apiKey;
$this->baseUrl = rtrim($baseUrl, '/');
}
private function request(string $method, string $path, array $body = null): array
{
$ch = curl_init($this->baseUrl . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'x-api-key: ' . $this->apiKey,
'Content-Type: application/json',
]);
if ($method === 'POST' || $method === 'PATCH') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
}
$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($response, true);
if (!$data['success']) {
throw new RuntimeException($data['error']['code'] . ': ' . $data['error']['message']);
}
return $data['data'];
}
public function findOrCreateContact(string $phone, string $name, ?string $utmSource = null): array
{
$body = ['phone' => $phone, 'name' => $name];
if ($utmSource !== null) {
$body['utmSource'] = $utmSource;
}
return $this->request('POST', '/partners/contacts', $body);
}
public function createBooking(array $bookingData): array
{
return $this->request('POST', '/partners/bookings', $bookingData);
}
public function getBookingStatus(string $bookingId): array
{
return $this->request('GET', "/partners/bookings/{$bookingId}/status");
}
public function cancelBooking(string $bookingId, string $notes = null): array
{
$body = $notes ? ['notes' => $notes] : [];
return $this->request('PATCH', "/partners/bookings/{$bookingId}/cancel", $body);
}
public function confirmPayment(string $bookingId, string $paymentReference = null): array
{
$body = $paymentReference ? ['paymentReference' => $paymentReference] : [];
return $this->request('POST', "/partners/bookings/{$bookingId}/confirm-payment", $body);
}
public static function verifyWebhook(string $secret, string $rawBody, string $timestamp, string $signatureHeader): bool
{
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $signatureHeader);
}
}
// Usage
$client = new CleanLifePartnerClient(
getenv('CLEANLIFE_API_KEY'),
getenv('CLEANLIFE_BASE_URL')
);
$booking = $client->createBooking([
'externalReference' => 'ORDER-20260615-001',
'contactId' => 'aaaaaaaa-0000-0000-0000-000000000001',
'addressId' => 'bbbbbbbb-0000-0000-0000-000000000002',
'serviceId' => 'dddddddd-0000-0000-0000-000000000004',
'hours' => 3,
'date' => '2026-06-20',
'timeslot' => ['startAt' => '09:00', 'endAt' => '12:00'],
]);
echo $booking['bookingId'];