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
- In CETECommand, open Jobs, Settings, API and webhooks (owners and admins).
- Make a key. It is shown once: keep it somewhere safe, like a password.
- 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" } }
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
- Base address:
https://app.cetechat.com/v1. JSON in, JSON out (Content-Type: application/json). - Money is in cents (
totalCents: 248000is $2,480.00), with acurrency(usuallyaud). Dates areYYYY-MM-DD; times are ISO 8601 in UTC. - Lists return
{ data, hasMore, nextBefore }, newest first. Use?limit=(1 to 100, default 25) and pass?before=<nextBefore>for the next page. - Errors return a status code and
{ "error": "What went wrong" }: 400 (check the input), 401 (key missing or wrong), 404, 429 (slow down), 500. - Rate limit: 120 requests a minute per key (headers
RateLimit-*say how many are left).
Enquiries
| Path | What it does | |
|---|---|---|
| GET | /v1/enquiries | List enquiries. ?status=new | converted | declined |
| GET | /v1/enquiries/:id | One enquiry |
| POST | /v1/enquiries | Send 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.
| Field | Notes |
|---|---|
name | Required |
email, phone | At least one |
address, service, message, preferred | Optional. preferred is the best time to come |
kind | quote (default) or work_order (when the company takes maintenance requests; needs address) |
company, orderNumber, lot, strataPlan, tenantName, tenantPhone, access, urgency | Work order details. urgency: routine, soon, urgent or emergency |
site, ip | Optional: 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
| Path | What it does | |
|---|---|---|
| GET | /v1/customers | List customers. ?email= (exact) or ?q= (name, email or phone) |
| GET | /v1/customers/:id | One customer |
| POST | /v1/customers | Add 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
| Path | What it does | |
|---|---|---|
| GET | /v1/jobs | List jobs. ?status=open | closed, ?customerId=, ?customerEmail= |
| GET | /v1/jobs/:id | One job: number, title, status, customer, site, tags, due date |
Quotes and invoices
| Path | What it does | |
|---|---|---|
| GET | /v1/documents | List sent quotes, invoices, work orders and credit notes. ?kind=invoice | quote | workorder | credit, ?status= (or unpaid), ?customerId=, ?customerEmail=, ?jobId= |
| GET | /v1/documents/:id | One 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:
| Event | When |
|---|---|
enquiry.created | A new enquiry (website form, booking page, email, chat or API) |
webchat.created | A visitor started a website chat |
job.created | A job was made |
job.status_changed | A job moved to another status |
quote.accepted | A customer accepted a quote or work order |
quote.declined | A customer declined a quote or work order |
invoice.paid | An 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", ... } }
- Answer with any 2xx within 8 seconds. Otherwise it is tried again after 30 seconds and after 5 minutes. Use
idto ignore repeats. - Addresses must be public HTTPS addresses. Redirects are not followed.
- Settings show the last 50 deliveries per webhook, and have a Send test button (event
ping).
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>
data-chaton the script adds the website chat button (when the company has it on).window.CETE.openChat()opens it from your own button.- The form element fires
cete:submitted(withdetail.kind) when sent, for analytics. - Your own form instead? POST JSON from the browser to
https://app.cetechat.com/e/BOOKING_KEY/apiwith the same fields as Send in an enquiry (allowed from the listed websites). - Client portal:
https://app.cetechat.com/p/BOOKING_KEY. Customers sign in with their email to see their jobs, approve quotes and pay invoices.
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.