CETECommand for developers

Connect a business's website, CRM or other systems to CETECommand: send in enquiries, read jobs, customers, quotes and invoices, and hear about changes the moment they happen.

Getting started

  1. In CETECommand, open Jobs, Settings, API and webhooks (owners and admins).
  2. Make a key. It is shown once: keep it somewhere safe, like a password.
  3. Call the API from your server:
curl https://app.cetechat.com/v1/ping \
  -H "Authorization: Bearer cete_sk_your_key"
{ "ok": true, "company": { "id": 12, "name": "Obsidian Renovations & Maintenance" }, "key": { "name": "Website", "prefix": "cete_sk_AbC123xy" } }
Server to server only. A secret key can read the company's customers and invoices. Never put it in a web page, a browser script or a phone app. For forms and chat on a website, use the website embeds: they use the public booking key instead.

Authentication

Send the key in the Authorization header on every request: Authorization: Bearer cete_sk_.... A key belongs to one company and sees only that company. Remove a key in Settings and it stops working straight away.

Conventions

Enquiries

PathWhat it does
GET/v1/enquiriesList enquiries. ?status=new | converted | declined
GET/v1/enquiries/:idOne enquiry
POST/v1/enquiriesSend in a new enquiry (for example from your own website form)

Send in an enquiry

It lands in Jobs, Enquiries with a note in the company's billing channel, the customer gets the usual confirmation email, and enquiry.created fires.

FieldNotes
nameRequired
email, phoneAt least one
address, service, message, preferredOptional. preferred is the best time to come
kindquote (default) or work_order (when the company takes maintenance requests; needs address)
company, orderNumber, lot, strataPlan, tenantName, tenantPhone, access, urgencyWork order details. urgency: routine, soon, urgent or emergency
site, ipOptional: the website it came from, and the visitor's IP address, for the record
curl -X POST https://app.cetechat.com/v1/enquiries \
  -H "Authorization: Bearer cete_sk_your_key" -H "Content-Type: application/json" \
  -d '{"name":"Jane Citizen","email":"jane@example.com","kind":"work_order","company":"Ray White","address":"12 Smith St, Campbelltown NSW","message":"Leak above the bathroom ceiling","urgency":"urgent"}'

Customers

PathWhat it does
GET/v1/customersList customers. ?email= (exact) or ?q= (name, email or phone)
GET/v1/customers/:idOne customer
POST/v1/customersAdd a customer: name (required), contact, email, phone, address, abn. With an email already on file, that customer comes back (200) instead of a new one (201)

Jobs

PathWhat it does
GET/v1/jobsList jobs. ?status=open | closed, ?customerId=, ?customerEmail=
GET/v1/jobs/:idOne job: number, title, status, customer, site, tags, due date

Quotes and invoices

PathWhat it does
GET/v1/documentsList sent quotes, invoices, work orders and credit notes. ?kind=invoice | quote | workorder | credit, ?status= (or unpaid), ?customerId=, ?customerEmail=, ?jobId=
GET/v1/documents/:idOne document, with its online link (view, pay by card, accept)

Drafts are never returned. link is the page the customer uses to view, pay or accept.

Webhooks

Add an HTTPS address in Jobs, Settings, API and webhooks and choose events. CETECommand POSTs JSON to it when they happen:

EventWhen
enquiry.createdA new enquiry (website form, booking page, email, chat or API)
webchat.createdA visitor started a website chat
job.createdA job was made
job.status_changedA job moved to another status
quote.acceptedA customer accepted a quote or work order
quote.declinedA customer declined a quote or work order
invoice.paidAn invoice was paid in full
POST /your/endpoint
X-Cete-Event: enquiry.created
X-Cete-Delivery: evt_5f1c...
X-Cete-Signature: t=1792396000,v1=3b9a...

{ "id": "evt_5f1c...", "type": "enquiry.created", "created": "2026-10-19T08:00:00.000Z", "data": { "id": 41, "name": "Jane Citizen", "kind": "work_order", ... } }

Check the signature

The signature is an HMAC-SHA256 of <t>.<raw body> with the webhook's secret (whsec_..., shown when you add it). Reject old timestamps to stop replays.

// Node.js (use the raw body, before JSON parsing)
import crypto from 'node:crypto';
function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const want = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(parts.v1 || ''));
}
// PHP
$raw = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_CETE_SIGNATURE'] ?? ''), $sig);
$want = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $raw, getenv('CETE_WEBHOOK_SECRET'));
if (!hash_equals($want, $sig['v1'] ?? '') || abs(time() - (int) ($sig['t'] ?? 0)) > 300) { http_response_code(400); exit; }
$event = json_decode($raw, true);

Website embeds

No secret key needed: these use the company's public booking key (Jobs, Settings, Booking page). List the website there first: only listed websites can show them.

<!-- once on every page -->
<script src="https://app.cetechat.com/w/v1.js" data-key="BOOKING_KEY" data-chat async></script>

<!-- enquiry form (data-type="work_order" or "quote" to show one kind only) -->
<div data-cete="enquiry-form" data-service="Roofing" data-title="Get a quote"></div>

<!-- link to the client portal -->
<div data-cete="client-login" data-label="Client login"></div>

WordPress and WooCommerce plugin

The CETECommand plugin does all of the above on a WordPress site without code: settings for the booking key (and an API key), the chat button on every page, [cete_enquiry_form] and [cete_client_login] shortcodes, and Contact Form 7 and WPForms submissions sent in as enquiries.

[cete_enquiry_form type="work_order" service="General maintenance" title="Log a maintenance request"]
[cete_client_login label="Client login"]

Install: download cetecommand.zip, then Plugins, Add New, Upload Plugin, choose it, Activate, then Settings, CETECommand.

WooCommerce (shown when WooCommerce is active, everything off until ticked): the chat on product, cart and checkout pages, a "Questions about this product?" link, an "Ask for a quote" button on products (the form opens with the product filled in), a "Your jobs and invoices" link in My Account, and orders into CETECommand: paste the shop orders address from Invoicing, Settings, Shop orders and press Connect, and the plugin sets up WooCommerce's own order webhooks for you.

[cete_quote_button]        <!-- on a product page: opens the form with the product -->
[cete_product_question]    <!-- "Questions about this product?" link -->
[cete_chat_button label="Chat with us"]

Node.js and PHP packages

Official packages wrap this API, the public enquiry address and webhook checks. Both have no dependencies.

# Node.js 18+ (ESM or CommonJS, TypeScript types, React components)
npm install cetecommand

import { CeteCommand, sendEnquiry, webhookMiddleware } from 'cetecommand';
const cete = new CeteCommand({ apiKey: process.env.CETE_API_KEY });
await cete.enquiries.create({ name: 'Sam', email: 'sam@example.com', message: 'Leaking tap' });

// React and Next.js
import { EnquiryForm, ChatButton } from 'cetecommand/react';
# PHP 7.4+ (plain PHP and Laravel)
composer require cetecommand/cetecommand

$cete = new \CeteCommand\Client(getenv('CETE_API_KEY'));
$cete->enquiries()->create(['name' => 'Sam', 'email' => 'sam@example.com', 'message' => 'Leaking tap']);
$event = \CeteCommand\Webhook::verify($rawBody, $_SERVER['HTTP_X_CETE_SIGNATURE'] ?? '', getenv('CETE_WEBHOOK_SECRET'));

Step-by-step guides for every website platform: cetecommand.com/connect.html.