Connect Halftonik to your other apps
Use Zapier to link thousands of apps with no code, or build your own with the REST API and webhooks. Every plan includes them.
Getting a key
In the app, go to Settings, then API and webhooks, and make a key. Read-only keys can look things up; read and write keys can also add customers and move jobs. The key is shown once. Each key belongs to one shop, and revoking it stops it straight away.
curl https://<your app address>/api/v1/me \
-H "Authorization: Bearer psk_..."- Up to 120 calls a minute per key. Over that, you get 429 and should wait a moment.
- Answers are JSON. Errors look like { "error": { "code", "message" } }.
- Lists return up to 25 items (?limit= up to 100) and a next_cursor. Pass it back as ?cursor= for the next page.
- Money is in cents and as dollars, e.g. { "cents": 148500, "aud": "1485.00" }, with GST stated.
- Only selling prices are shared. Your costs and margins never leave the app.
Endpoints
All under /api/v1.
GET | /me | Which shop the key is for. Good for checking a key works. |
GET | /customers | Customers, newest first. ?q= searches name and email. ?updated_since= takes an ISO time. |
POST | /customers | Add a customer (write key). Name is required; ABN and email are checked. |
GET | /customers/{id} | One customer, with their contacts. |
GET | /jobs | Quotes and jobs. Filter with ?status=, ?stage=, ?customer_id=, ?updated_since=. |
GET | /jobs/{id} | One job, with its garments and sizes. |
PATCH | /jobs/{id} | Move a job on the board (write key): { "stage": "printing" }. |
GET | /invoices | Tax invoices. Filter with ?status=unpaid|paid|void, ?customer_id=. |
GET | /invoices/{id} | One invoice, with its lines. |
POST | /hooks | Subscribe to events: { "url": "https://...", "events": ["job.stage_changed"] }. |
DELETE | /hooks/{id} | Unsubscribe a webhook that this key added. |
GET | /samples/{event} | The latest few messages for an event, as a webhook would get them. |
Webhooks
Add an https address in Settings, or subscribe through the API, and we POST a message there when something happens. Each message is signed in the Standard Webhooks format, sent at least once (use the id to ignore repeats), and tried again for about a day if your address doesn't answer with a 2xx.
customer.created | A customer is added |
quote.sent | A quote is sent |
quote.approved | A quote is approved |
quote.changes_requested | A customer asks for changes to a quote |
proof.approved | A customer approves a proof |
job.stage_changed | A job moves to another stage |
job.shipped | A job is sent to the customer |
order.received | An order comes in from your order page |
invoice.issued | An invoice is sent |
payment.received | A payment comes in |
What a message looks like
{
"id": "1f0c…",
"type": "job.stage_changed",
"timestamp": "2026-10-23T03:15:00.000Z",
"data": {
"job": {
"id": "8a2e…", "number": "#1042", "title": "Club polos",
"status": "approved", "stage": "ready_to_go", "stage_label": "Ready to go",
"due_date": "2026-11-20", "quantity": 60,
"total_inc_gst": { "cents": 148500, "aud": "1485.00" }
},
"customer": { "id": "c41d…", "name": "Riverbend FC", "email": "office@riverbend.example", "phone": "03 9555 0101" },
"contact": { "name": "Jordan Lee", "email": "jordan@riverbend.example", "phone": "0412 345 678" },
"changes": { "from": "finishing", "to": "ready_to_go" },
"link": "https://…/q/…"
}
}Checking a message came from us
Each message has webhook-id, webhook-timestamp and webhook-signature headers. The signature is an HMAC-SHA256 of id, timestamp and the raw body, using your webhook's signing secret:
import { createHmac, timingSafeEqual } from "node:crypto";
// secret is the webhook's signing secret, starting whsec_
export function isFromUs(secret, headers, rawBody) {
const id = headers["webhook-id"];
const ts = headers["webhook-timestamp"];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // older than 5 minutes
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest("base64");
return headers["webhook-signature"].split(" ").some((sig) => {
const [version, value] = sig.split(",");
const a = Buffer.from(value ?? "");
const b = Buffer.from(expected);
return version === "v1" && a.length === b.length && timingSafeEqual(a, b);
});
}Zapier
Use Zapier's own Webhooks by Zapier to connect today. To start a Zap when something happens, choose "Catch Hook" as the trigger and add the address Zapier gives you as a webhook in Settings. To change something in Halftonik from a Zap, choose "Custom Request" and call the API with your key in the Authorization header.