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
}
]
}| Field | Required | Meaning |
|---|---|---|
externalId | yes | Your own id for the transaction. Sending the same id again is ignored, so retries and re-runs never double count. |
occurredAt | yes | When the sale happened, ISO 8601 with a timezone (-06:00 or Z). Sales roll up by hour. |
grossSales | yes | Gross sales in dollars, not cents. |
netSales | no | Net 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:
acceptedwere new,duplicateswere already recorded,bucketsis 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
- Trigger: a new order in your POS, or a scheduled run over a report.
- Action: Webhooks by Zapier → Custom Request (or an HTTP module in Make). Method POST, URL
https://hours-ai.com/api/ingest/sales. - Headers:
Authorization: Bearer hai_sk_YOUR_KEYandContent-Type: application/json. - 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.