Skip to content

Developer docs

Connect any POS.

One request sends sales to Hours AI. They land beside clocked hours for labor vs sales, exactly like a Square sale does. Retries are safe.

1. Get a key

In Hours AI, open Settings → POS integrations → Sales ingest keys, name the key after where it will be used (Zapier, Nightly export, Toast bridge) and create it. The key is shown once; copy it somewhere safe. You can rotate or revoke it any time.

2. Check the key

A GET with the key writes nothing and tells you which location it belongs to. Most no-code tools use this as the “test connection” step.

curl https://hours-ai.com/api/ingest/sales \
  -H "Authorization: Bearer hai_sk_YOUR_KEY"

3. Send sales

POST the same URL. Up to 500 sales per request, up to 100 requests a minute per key.

curl -X POST https://hours-ai.com/api/ingest/sales \
  -H "Authorization: Bearer hai_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"sales": [{"externalId": "order-1001","occurredAt": "2026-09-10T19:12:00-06:00","grossSales": 42.50,"netSales": 40.00},{"externalId": "order-1002","occurredAt": "2026-09-10T19:40:00-06:00","grossSales": 18.25}]}'

The body, formatted:

{
  "sales": [
    {
      "externalId": "order-1001",
      "occurredAt": "2026-09-10T19:12:00-06:00",
      "grossSales": 42.50,
      "netSales": 40.00
    },
    {
      "externalId": "order-1002",
      "occurredAt": "2026-09-10T19:40:00-06:00",
      "grossSales": 18.25
    }
  ]
}
FieldRequiredMeaning
externalIdyesYour own id for the transaction. Sending the same id again is ignored, so retries and re-runs never double count.
occurredAtyesWhen the sale happened, ISO 8601 with a timezone (-06:00 or Z). Sales roll up by hour.
grossSalesyesGross sales in dollars, not cents.
netSalesnoNet of discounts and comps, if you have it.

A bare array of sales, or a single sale object, is accepted too, so a Zapier or Make step that emits one order at a time works without reshaping.

4. Read the response

200 { "ok": true, "received": 2, "accepted": 2, "duplicates": 0, "buckets": 1 }
  • 200 with counts: accepted were new, duplicates were already recorded, buckets is how many hours changed.
  • 401 wrong or missing key · 409 revoked key · 413 body over 1 MB · 422 invalid payload, with the fields and reasons · 429 slow down.

Every request is summarised on the key in Settings, so a manager can see it working, or see what a step got wrong, without asking anyone.

Recipes

Zapier or Make

  1. Trigger: a new order in your POS, or a scheduled run over a report.
  2. Action: Webhooks by Zapier → Custom Request (or an HTTP module in Make). Method POST, URL https://hours-ai.com/api/ingest/sales.
  3. Headers: Authorization: Bearer hai_sk_YOUR_KEY and Content-Type: application/json.
  4. Body: map the order id, time, gross and net into the four fields. One order per run is fine.

Google Sheets

A script bound to the sheet, run on a timer from Extensions → Apps Script → Triggers.

// Google Sheets: columns A=order id, B=time (ISO), C=gross, D=net
function sendSalesToHoursAI() {
  const rows = SpreadsheetApp.getActiveSheet().getDataRange().getValues().slice(1);
  const sales = rows.map(([id, time, gross, net]) => ({
    externalId: String(id),
    occurredAt: new Date(time).toISOString(),
    grossSales: Number(gross),
    netSales: net === "" ? undefined : Number(net),
  }));
  UrlFetchApp.fetch("https://hours-ai.com/api/ingest/sales", {
    method: "post",
    contentType: "application/json",
    headers: { Authorization: "Bearer hai_sk_YOUR_KEY" },
    payload: JSON.stringify({ sales }),
  });
}

Your own POS or back office

Batch the day’s orders and POST them nightly, or send each order as it closes. Use the order id as externalIdand you can re-run any day’s export safely.

Security

  • Only a hash of the key is stored; it is compared in constant time.
  • Keys belong to one location and can be rotated or revoked in Settings.
  • Send over HTTPS only. Never put the key in a URL.

Questions, or a POS that could push to us directly? Tell us what you run on.