Agency integration guide

How a staffing agency connects TempClock to its payroll, booking, site and billing systems, step by step, with the exact calls, scopes and webhooks for each.

The weekly cycle

Everything in TempClock turns on the pay week (your pay week start day, Monday by default). A shift belongs to the pay week of its clock-in date. Through the week:

WhenIn TempClockWhat an integration does
Before the weekRequirements and bookings are madeYour booking system creates requirements and books workers (recipe 2).
During the weekWorkers clock at the kiosk, the app or your own terminalsClocks arrive from other hardware (recipe 4); site boards show who is on site (recipe 3).
Each dayCoordinators approve shifts; the client signs off and may query or amendListen for timesheet.approved, timesheet.client_rejected, timesheet.queried, timesheet.amended; read /exceptions.php for what needs attention.
After the week endsThe week is checked and locked on the Close pay week page, and the Sage file is madepay_week.locked fires; payroll pulls the week (recipe 1); billing pulls charges (recipe 5).
LaterA clock that lands in the locked week (for example an offline kiosk) is kept and flaggedIt is paid as an adjustment the next week: adjustments=1 on payroll, late_adjustments_due on the pay week.

Recipe 1: payroll per pay week, with Sage batches

Key: payroll:read (it includes pay), plus webhooks:write to subscribe. Add charges:read only if the payroll system needs charge totals.

There are two ways to get pay into Sage 50 Payroll, and both give the same figures, because the payroll endpoint pays exactly the shifts the Sage file pays, with the same rates, elements and totals:

  • The file: payroll staff lock the week and download the Sage 50 Payroll file in TempClock. The API is then only used to confirm the week is final and to reconcile (steps 1 and 4 below).
  • The API: the payroll system pulls the lines itself (all four steps).
  1. Wait for the lock. Subscribe to pay_week.locked (and pay_week.unlocked). As a fallback, poll GET /pay-weeks.php once an hour after the week ends: status goes open → checking → ready_to_lock → locked → exported. Before the lock, exceptions_open and shifts_not_approved say what is left.
  2. Pull the lines. GET /payroll.php?week=<week_start>&limit=200&adjustments=1, paging with offset. Import pay_elements: each line carries your Sage payment reference (sage_ref), hours, rate, amount, department and cost centre. source: locked shows the frozen figures were used.
  3. Pay late clocks. Rows in adjustments with state: due and adjust_week equal to this week are owed now. They are not in an earlier week's lines, so nothing is paid twice.
  4. Reconcile. The pay week's batches list every file made for it, with file_sha256 and totals. GET /export-batches.php?id= lists the shifts a file held, recorded when it was made (Sage, Xero and payroll files); each shift also carries exported_batch_id on /timesheets.php, the latest Sage or Xero file that held it. export.created fires when a file is made.
Is the pay week final, and what was exported? 200

Key used: a key with payroll:read only (pay figures, no charges).

Request

curl -s "$TC_BASE/pay-weeks.php?week_start=2026-09-25" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "week_start": "2026-09-21",
      "week_end": "2026-09-27",
      "status": "exported",
      "ended": true,
      "locked": true,
      "locked_at": "2026-10-01 07:33:35",
      "locked_at_utc": "2026-10-01T06:33:35Z",
      "locked_by": null,
      "lock_id": 19,
      "frozen": false,
      "shifts": 107,
      "shifts_not_approved": 29,
      "open_clock_outs": 0,
      "exceptions_open": 0,
      "late_adjustments_due": 0,
      "late_adjustments_paid": 0,
      "late_clocks_in_week": 1,
      "batches": [
        {
          "id": 8,
          "kind": "sage",
          "format": "sage50",
          "client_id": 21,
          "period_start": "2026-09-21",
          "period_end": "2026-09-27",
          "entry_count": 2,
          "line_count": 4,
          "file_name": "sage50_payroll_2026-09-21.csv",
          "file_sha256": "2393f9791223b6c74c664b8ad9549911439b7e9f10057d5c5ae9b9807b36f322",
          "reason": null,
          "created_at": "2026-10-02 07:33:35",
          "created_at_utc": "2026-10-02T06:33:35Z",
          "stamps_entries": true,
          "total_pay": 241.5
        }
      ]
    }
  ],
  "meta": {
    "period_from": "2026-09-21",
    "period_to": "2026-09-27",
    "pay_week_start_day": 1,
    "approval_rule": "agency_approved"
  }
}
Payroll lines for one pay week (payroll:read key: pay figures, no charges) 200

Key used: a key with payroll:read only (pay figures, no charges).

Request

curl -s "$TC_BASE/payroll.php?week=2026-09-25&limit=2" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "worker_id": 351,
      "employee_id": "AX025",
      "worker_name": "Aisha Rahman",
      "pay_week_start": "2026-09-21",
      "pay_week_end": "2026-09-27",
      "shifts": 1,
      "paid_minutes": 168,
      "paid_hours": 2.8,
      "gross_pay": 37.8,
      "base_pay_rate": 13.5,
      "department": "WH-OPS",
      "cost_centre": "WH-OPS",
      "elements": {
        "regular": {
          "minutes": 168,
          "hours": 2.8,
          "pay": 37.8,
          "pay_rate": 13.5
        },
        "overtime": {
          "minutes": 0,
          "hours": 0,
          "pay": 0,
          "pay_rate": null
        },
        "weekend": {
          "minutes": 0,
          "hours": 0,
          "pay": 0,
          "pay_rate": null
        },
        "bank_holiday": {
          "minutes": 0,
          "hours": 0,
          "pay": 0,
          "pay_rate": null
        },
        "night": {
          "minutes": 0,
          "hours": 0,
          "pay": 0,
          "pay_rate": null
        }
      },
      "pay_elements": [
        {
          "element": "regular",
          "label": "Basic",
          "sage_ref": "Basic",
          "minutes": 168,
          "hours": 2.8,
          "pay_rate": 13.5,
          "pay": 37.8,
          "department": "WH-OPS",
          "cost_centre": "WH-OPS"
        }
      ],
      "source": "live",
      "flags": []
    },
    {
      "worker_id": 337,
      "employee_id": "AX011",
      "worker_name": "Amira Patel",
      "pay_week_start": "2026-09-21",
      "pay_week_end": "2026-09-27",
      "shifts": 5,
      "paid_minutes": 2098,
      "paid_hours": 34.97,
      "gross_pay": 472.05,
      "base_pay_rate": 13.5,
      "department": "MAINT",
      "cost_centre": "MAINT",
      "elements": {
        "regular": {
          "minutes": 2098,
          "hours": 34.97,
  ... (82 more lines)
One batch with the shifts it paid 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/export-batches.php?id=8" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 8,
    "kind": "sage",
    "format": "sage50",
    "client_id": 21,
    "period_start": "2026-09-21",
    "period_end": "2026-09-27",
    "entry_count": 2,
    "line_count": 4,
    "file_name": "sage50_payroll_2026-09-21.csv",
    "file_sha256": "2393f9791223b6c74c664b8ad9549911439b7e9f10057d5c5ae9b9807b36f322",
    "reason": null,
    "created_at": "2026-10-02 07:33:35",
    "created_at_utc": "2026-10-02T06:33:35Z",
    "stamps_entries": true,
    "total_pay": 241.5,
    "total_charge": 362.25,
    "entry_ids": [
      36686,
      36688
    ],
    "file_ok": false
  }
}

Which shifts are paid. Closed, approved by the agency, not rejected or disputed by the client and with no open query, signed off by the client when the client must sign off, with a pay rate, and not already paid as a late adjustment. GET /timesheets.php?payroll_status=on_hold lists the rest with the reason in payroll_excluded_reason, so payroll can chase them before the lock.

Recipe 2: booking sync

Key: shifts:write, availability:read, absences:read, workers:read, locations:read. No pay access.

  1. Create the requirement the client asked for: POST /demands.php with site, date, times, headcount and role.
  2. Find who is free: GET /availability.php?available_from=&available_to=&location_id=&job_role=. The answer uses the Shifts board's own rules (approved absence, marked unavailable, do not return, overlapping booking, rest, weekly hours), best matches first, with codes only.
  3. Book: POST /shifts.php with worker_id and demand_id. A hard block is 409 not_eligible with block_codes; the API has no override.
  4. Change or cancel: PUT /shifts.php?id= (times, break, role, worker, site); POST /shifts.php?id=&action=cancel with client_cancelled, worker_cancelled, sick, awol or no_show. The worker is told, as in the console. A worked booking or one in a locked week is refused (reason_code worked or week_locked).
  5. Stay in step with changes made in TempClock: subscribe to shift.created, shift.updated, shift.cancelled and shift.no_show, and run a nightly GET /shifts.php?updated_since= as a safety net. Offers taken or declined by workers, and cover, send no shift event yet: the updated_since read catches them. GET /offers.php?demand_id= shows who was offered a requirement and how they answered.
Create a requirement: 3 people for an early shift 201

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/demands.php" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"location_id":37,"shift_date":"2026-10-28","start_time":"06:00","end_time":"14:00","headcount":3,"break_minutes":30,"job_role":"Picker"}'

Real reply (local test copy, fake demo data)

HTTP 201

{
  "data": {
    "id": 3066,
    "client_id": 21,
    "location_id": 37,
    "location_name": "London HQ",
    "shift_date": "2026-10-28",
    "start_time": "06:00:00",
    "end_time": "14:00:00",
    "start_utc": "2026-10-28T06:00:00Z",
    "end_utc": "2026-10-28T14:00:00Z",
    "break_minutes": 30,
    "job_role": "Picker",
    "headcount": 3,
    "requested": 3,
    "filled": 0,
    "open": 3,
    "status": "open",
    "series_id": null,
    "source": "api",
    "notes": null,
    "cancel_reason": null,
    "cancelled_at": null,
    "created_at": "2026-10-02 07:33:39",
    "created_at_utc": "2026-10-02T06:33:39Z",
    "updated_at": "2026-10-02 07:33:39",
    "updated_at_utc": "2026-10-02T06:33:39Z"
  },
  "message": "Requirement created."
}
Who is free for a slot at a site, best matches first 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/availability.php?available_from=2026-10-28T07%3A00%3A00%2B01%3A00&available_to=2026-10-28T15%3A00%3A00%2B01%3A00&location_id=37&limit=2" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": [
    {
      "worker_id": 328,
      "name": "Emma Thompson",
      "employee_id": "AX002",
      "location_id": 37,
      "department_id": 41,
      "job_role": "Administrator",
      "eligible": true,
      "available": true,
      "availability": [
        "06:00-22:00"
      ],
      "role_match": null,
      "week_hours": 28,
      "max_hours_per_week": 48,
      "warning_codes": [],
      "block_codes": []
    },
    {
      "worker_id": 340,
      "name": "George Taylor",
      "employee_id": "AX014",
      "location_id": 38,
      "department_id": 40,
      "job_role": "Warehouse Operative",
      "eligible": true,
      "available": true,
      "availability": [
        "06:00-22:00"
      ],
      "role_match": null,
      "week_hours": 28.5,
      "max_hours_per_week": 48,
      "warning_codes": [],
      "block_codes": []
    }
  ],
  "pagination": {
    "total": 10,
    "limit": 2,
    "offset": 0,
    "has_more": true
  },
  "meta": {
    "slot": {
      "shift_date": "2026-10-28",
      "start_time": "06:00:00",
      "end_time": "14:00:00",
      "start_utc": "2026-10-28T06:00:00Z",
      "end_utc": "2026-10-28T14:00:00Z",
      "location_id": 37,
      "job_role": null
    }
  }
}
Book a worker onto the requirement 201

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/shifts.php" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"worker_id":409,"demand_id":3066,"status":"published"}'

Real reply (local test copy, fake demo data)

HTTP 201

{
  "data": {
    "id": 31188,
    "client_id": 21,
    "worker_id": 409,
    "worker_name": "Jordan Example",
    "employee_id": "DOCS-77E88",
    "department_id": 40,
    "location_id": 37,
    "location_name": "London HQ",
    "demand_id": 3066,
    "shift_date": "2026-10-28",
    "start_time": "06:00:00",
    "end_time": "14:00:00",
    "start_utc": "2026-10-28T06:00:00Z",
    "end_utc": "2026-10-28T14:00:00Z",
    "break_minutes": 30,
    "job_role": "Picker",
    "notes": null,
    "status": "published",
    "cancel_reason": null,
    "cancelled_at": null,
    "acknowledged_at": null,
    "declined_at": null,
    "confirmation": "unconfirmed",
    "created_at": "2026-10-02 07:33:39",
    "created_at_utc": "2026-10-02T06:33:39Z",
    "updated_at": "2026-10-02 07:33:39",
    "updated_at_utc": "2026-10-02T06:33:39Z",
    "warning_codes": [
      "role"
    ]
  },
  "message": "Shift created."
}
Cancel a booking (the client no longer needs it) 200

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/shifts.php?id=31187&action=cancel" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":"client_cancelled","cancel_reason":"Client reduced headcount"}'

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "id": 31187,
    "client_id": 21,
    "worker_id": 409,
    "worker_name": "Jordan Example",
    "employee_id": "DOCS-77E88",
    "department_id": 40,
    "location_id": 37,
    "location_name": "London HQ",
    "demand_id": null,
    "shift_date": "2026-10-27",
    "start_time": "09:00:00",
    "end_time": "17:00:00",
    "start_utc": "2026-10-27T09:00:00Z",
    "end_utc": "2026-10-27T17:00:00Z",
    "break_minutes": 30,
    "job_role": "Warehouse Operative",
    "notes": "Bring safety boots",
    "status": "client_cancelled",
    "cancel_reason": "Client reduced headcount",
    "cancelled_at": "2026-10-02 07:33:39",
    "acknowledged_at": null,
    "declined_at": null,
    "confirmation": "n/a",
    "created_at": "2026-10-02 07:33:38",
    "created_at_utc": "2026-10-02T06:33:38Z",
    "updated_at": "2026-10-02 07:33:39",
    "updated_at_utc": "2026-10-02T06:33:39Z"
  },
  "message": "Booking client cancelled."
}

Recipe 3: live on-site feed

Key: onsite:read only (no worker records, no pay), one key per board. The key sees every site of the account; filter with location_id for one board.

  • Poll GET /on-site-now.php every 30 to 60 seconds (well inside 60 requests a minute). include_workers=false gives totals only, for a headcount display.
  • "On site" is the kiosk's own rule: an open shift clocked in within the last 18 hours, so a night shift stays on the list across midnight and a forgotten clock-out from days ago does not.
  • within_geofence is 1 inside, 0 outside and null when unknown; unknown counts as on site.
  • Build the board from /on-site-now.php, not from the clock_in and clock_out webhooks. In v1.3 a clock made at the kiosk or in the worker app is sent to the worker's home client, so a pooled worker clocking at your site sends you no event (see Webhooks). /on-site-now.php lists everyone at your sites, pooled workers included.
Who is on site now, by site 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/on-site-now.php?location_id=37" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "as_of": "2026-10-02T06:33:35Z",
    "totals": {
      "clocked_in": 1,
      "on_site": 1,
      "off_site": 0,
      "geofence_unknown": 1,
      "low_gps_confidence": 0
    },
    "locations": [
      {
        "location_id": 37,
        "location_name": "London HQ",
        "clocked_in": 1,
        "on_site": 1,
        "off_site": 0,
        "geofence_unknown": 1,
        "workers": [
          {
            "worker_id": 409,
            "name": "Jordan Example",
            "employee_id": "DOCS-77E88",
            "entry_id": 40785,
            "clock_in": "2026-10-02 07:19:35",
            "clock_in_utc": "2026-10-02T06:19:35Z",
            "minutes_on_shift": 14,
            "within_geofence": null,
            "distance_meters": null,
            "gps_accuracy": null,
            "low_gps_confidence": false
          }
        ]
      }
    ],
    "unassigned": []
  }
}
Totals only, with an onsite:read key (a site board) 200

Key used: a key with onsite:read only.

Request

curl -s "$TC_BASE/on-site-now.php?include_workers=false" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "as_of": "2026-10-02T06:33:35Z",
    "totals": {
      "clocked_in": 1,
      "on_site": 1,
      "off_site": 0,
      "geofence_unknown": 1,
      "low_gps_confidence": 0
    },
    "locations": [
      {
        "location_id": 37,
        "location_name": "London HQ",
        "clocked_in": 1,
        "on_site": 1,
        "off_site": 0,
        "geofence_unknown": 1
      }
    ],
    "unassigned": []
  }
}

Recipe 4: clock feed from other hardware

Key: clock:write only, with the rate limit raised for shift changes.

  1. Name the worker by the badge number printed on their card: employee_id (or worker_id).
  2. Send occurred_at, the time of the read. Up to 15 minutes back is accepted; more than 60 seconds back is flagged api_backdated for review. Send an Idempotency-Key made from the read, so a retry never clocks twice.
  3. TempClock links the clock to the worker's booking that covers the time (scheduled_shift_id), applies the site's geofence (a strict site needs latitude and longitude: send the terminal's fixed position), and refuses pending and inactive workers, like the kiosk.
  4. Handle the answers: already_clocked_in and no_open_clock_in are double reads; worker_pending, worker_inactive and not_found are people who should not be clocking.

Throughput: at 60 requests a minute, 450 people take about 7.5 minutes to send; with occurred_at their hours are right either way. Raise the key to 120 a minute (under 4 minutes), or ask TempClock for up to 600. See Rate limits and the code example.

Clock in by employee ID, with the time it really happened 201

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/clock.php" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: docs-clock-77e88" \
  -d '{"employee_id":"DOCS-77E88","action":"in","location_id":37,"occurred_at":"2026-10-02T06:19:35Z"}'

Real reply (local test copy, fake demo data)

HTTP 201

{
  "data": {
    "entry_id": 40785,
    "worker_id": 409,
    "employee_id": "DOCS-77E88",
    "worker_name": "Jordan Example",
    "action": "clock_in",
    "clock_in": "2026-10-02 07:19:35",
    "clock_in_utc": "2026-10-02T06:19:35Z",
    "location_id": 37,
    "scheduled_shift_id": null,
    "risk_flags": [
      "api_backdated"
    ],
    "backdated": true
  },
  "message": "Jordan Example clocked in at 07:19"
}
Clock in from outside a warning-mode geofence, with a poor GPS reading: accepted and flagged 201

Key used: a key holding every scope.

Request

curl -s -X POST "$TC_BASE/clock.php" \
  -H "Authorization: Bearer $TC_KEY" \
  -H "Content-Type: application/json" \
  -d '{"worker_id":409,"action":"in","location_id":39,"latitude":52.5264,"longitude":-1.8998,"gps_accuracy":150}'

Real reply (local test copy, fake demo data)

HTTP 201

{
  "data": {
    "entry_id": 40786,
    "worker_id": 409,
    "employee_id": "DOCS-77E88",
    "worker_name": "Jordan Example",
    "action": "clock_in",
    "clock_in": "2026-10-02 07:33:36",
    "clock_in_utc": "2026-10-02T06:33:36Z",
    "location_id": 39,
    "scheduled_shift_id": null,
    "risk_flags": [
      "geofence_outside",
      "low_gps"
    ],
    "backdated": false,
    "within_geofence": 0,
    "distance_meters": 5004,
    "low_gps_confidence": true,
    "warning": "Clocked in 5004m from Birmingham Office, recorded."
  },
  "message": "Jordan Example clocked in at 07:33 (Clocked in 5004m from Birmingham Office, recorded.)"
}

Recipe 5: invoicing

Key: charges:read and payroll:read (for the pay week status).

GET /charges.php?week_start=&vat=true returns the charge schedule: lines per site, worker, role and pay element, site totals with billing name, PO, Sage account reference, department and nominal code, VAT, and what was held back and why. A locked week returns its frozen lines (frozen: true), so an invoice never changes after the lock. It is the same builder as the Charge schedule page and its Sage and Xero files.

Charge schedule for one pay week, with VAT 200

Key used: a key holding every scope.

Request

curl -s "$TC_BASE/charges.php?week_start=2026-09-25&vat=true" \
  -H "Authorization: Bearer $TC_KEY"

Real reply (local test copy, fake demo data)

HTTP 200

{
  "data": {
    "reference": "CS21-20260921",
    "client_id": 21,
    "from": "2026-09-21",
    "to": "2026-09-27",
    "frozen": false,
    "partly_frozen": false,
    "require_client_approval": false,
    "currency": "GBP",
    "lines": [
      {
        "site_id": 39,
        "site_name": "Birmingham Office",
        "worker_id": 351,
        "worker_name": "Aisha Rahman",
        "employee_id": "AX025",
        "role": "Customer Service Agent",
        "cost_code": "WH-OPS",
        "po": null,
        "element": "regular",
        "element_label": "Standard",
        "hours": 2.8,
        "rate": 18.9,
        "amount": 52.92,
        "shifts": 1,
        "first_date": "2026-09-24",
        "last_date": "2026-09-24",
        "frozen": false,
        "no_charge": false
      },
      {
        "site_id": 39,
        "site_name": "Birmingham Office",
        "worker_id": 337,
        "worker_name": "Amira Patel",
        "employee_id": "AX011",
        "role": "Administrator",
        "cost_code": "MAINT",
        "po": null,
        "element": "regular",
        "element_label": "Standard",
        "hours": 34.97,
        "rate": 18.9,
        "amount": 660.87,
        "shifts": 5,
        "first_date": "2026-09-21",
        "last_date": "2026-09-25",
        "frozen": false,
        "no_charge": false
      },
      {
        "site_id": 39,
        "site_name": "Birmingham Office",
        "worker_id": 339,
        "worker_name": "Charlotte Evans",
        "employee_id": "AX013",
        "role": "Customer Service Agent",
        "cost_code": "WH-OPS",
        "po": null,
        "element": "regular",
        "element_label": "Standard",
        "hours": 16.43,
        "rate": 18.9,
        "amount": 310.6,
        "shifts": 3,
        "first_date": "2026-09-21",
        "last_date": "2026-09-24",
        "frozen": false,
        "no_charge": false
      },
      {
        "site_id": 39,
        "site_name": "Birmingham Office",
        "worker_id": 342,
        "worker_name": "Hannah Williams",
        "employee_id": "AX016",
        "role": "Customer Service Agent",
        "cost_code": "LOG-DRIV",
        "po": null,
  ... (561 more lines)

Pooled workers and several clients

  • A worker can be pooled: their home client is one account, and they work at another client's site. A time record, booking or payroll line belongs to the client that owns the site, so the site's client sees and pays it, exactly as in the Sage file.
  • A key belongs to one client account and covers all its sites.
  • An agency-wide key that covers several client accounts is planned and not available in v1.3. Until then, make one key per client account and run each integration per account. Webhook payloads name the account in data.client_id, so one receiver can serve several accounts, with one exception in v1.3: clocks from the kiosk and the worker app carry no client_id and go to the worker's home client. Nothing in their payload or headers names the subscription, so give each account's subscription its own receiver URL (for example https://hooks.example.com/tempclock?account=21) and route on that, or check the signature with each account's signing secret and use the account whose secret verifies X-TempClock-Signature. For API replies, use the key you called with to know the account.

Go-live checklist

  1. API access is switched on for the account; one key per system, each with only the scopes it needs (recipes).
  2. Keys stored in a secret store; an expiry date where the integration is temporary; a rotation date in the calendar.
  3. Webhook receivers check the signature, answer within 5 seconds and de-duplicate on X-TempClock-Delivery; a test delivery has reached each one.
  4. Every write sends an Idempotency-Key; 429 and 5xx are retried with backoff; X-Request-Id is logged.
  5. Sync jobs use updated_since and de-duplicate on id; times are stored from the *_utc fields.
  6. Someone checks Setup > API logs for failures in the first weeks.