Skip to main content
Complete code examples for common Partner API operations across multiple languages.

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)...
For Production, change 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'];