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/meWhich shop the key is for. Good for checking a key works.
GET/customersCustomers, newest first. ?q= searches name and email. ?updated_since= takes an ISO time.
POST/customersAdd a customer (write key). Name is required; ABN and email are checked.
GET/customers/{id}One customer, with their contacts.
GET/jobsQuotes 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/invoicesTax invoices. Filter with ?status=unpaid|paid|void, ?customer_id=.
GET/invoices/{id}One invoice, with its lines.
POST/hooksSubscribe 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.createdA customer is added
quote.sentA quote is sent
quote.approvedA quote is approved
quote.changes_requestedA customer asks for changes to a quote
proof.approvedA customer approves a proof
job.stage_changedA job moves to another stage
job.shippedA job is sent to the customer
order.receivedAn order comes in from your order page
invoice.issuedAn invoice is sent
payment.receivedA 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.

Start free trial