HubstaffDeveloper Portal

Getting started

Webhooks

Webhooks deliver real-time notifications when events happen in your Hubstaff organization — timer changes, project/task updates, schedule events, billing snapshots, and more. Three endpoints (create → activate → delete) plus a verification handshake your endpoint implements.

Requirements
  • OAuth scope: hubstaff:write to create / activate / delete, hubstaff:read to inspect.
  • Scope your webhook to the right owner: org-wide (/organizations/{org_id}/webhooks), single project (/projects/{project_id}/webhooks), or the current user (/users/me/webhooks).

Step 1 — Create the webhook

Returns a webhook id and starts the verification handshake (Step 2) immediately. The webhook is inactive until you complete Step 3.

bash
curl -X POST https://api.hubstaff.com/v2/organizations/<org_id>/webhooks \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "events": ["timer.start", "timer.stop"],
    "target_url": "https://your-tunnel.ngrok.app/webhook"
  }'

Use ["*"] to subscribe to every event (see the Events table below for the full list).

Step 2 — Verify your endpoint

Right after Step 1, Hubstaff sends an empty-body POST to your target_url with one header:

bash
POST /webhook HTTP/1.1
Host: your-tunnel.ngrok.app
Content-Length: 0
X-Hook-Secret: 8f3c1a…   ← unique per webhook, store this

Your endpoint must respond 200 OK and echo the same header back. The verification ping is async — Hubstaff hits your endpoint within ~15 seconds after Step 1, so make sure the endpoint is live first.

bash
HTTP/1.1 200 OK
X-Hook-Secret: 8f3c1a…
Content-Length: 0
Save the secret
The secret you echoed back is also the HMAC key you'll use to verify every incoming event (Step 5). Persist it server-side, scoped to this webhook's id. It is never returned by any other API call.

Step 3 — Activate

Send the secret you saved in Step 2 back to Hubstaff as the X-Hook-Secret header. After this returns 200, events start flowing to target_url.

bash
curl -X POST https://api.hubstaff.com/v2/webhooks/<webhook_id>/activate \
  -H "Authorization: Bearer <access_token>" \
  -H "X-Hook-Secret: 8f3c1a…"

Step 4 — Receive events

Every delivery has this shape:

bash
POST /webhook HTTP/1.1
Content-Type: application/json
X-Hook-Signature: 1d4e2c…   ← HMAC-SHA256(body, secret)
X-Product-Id: hubstaff

{
  "id": "69d37f9f-483e-4be1-92c3-b2377203a8cd",
  "event": "timer.start",
  "created_at": "2024-06-20T14:10:55.196912",
  "payload": {
    "organization_id": 13,
    "project_id": 841201,
    "project_name": "DevSecOps",
    "task_id": null,
    "user_id": 651956,
    "user_name": "Alex Yarotsky",
    "tracking_started_at": "2024-06-20T14:10:46.235Z"
  }
}

Step 5 — Verify the signature

Always verify before processing. Compute HMAC-SHA256 of the raw request body using the secret you saved in Step 2 as the key, compare it to X-Hook-Signature in constant time. Anything that doesn't match — drop with 401.

bash
# Sanity-check a real delivery with openssl:
echo -n '<raw request body bytes>' \
  | openssl dgst -sha256 -hmac '<secret>' -hex
# Should print the same hex string that came in X-Hook-Signature.

Statuses & failure handling

GET /v2/webhooks/<id> returns a status field. What it means:

StatusMeaning
inactive Just created. Verification ping hasn't been sent yet (~15s queue delay).
active Verification succeeded — endpoint replied 2xx and echoed X-Hook-Secret. Events are flowing.
pending Endpoint replied 2xx but did not echo the header. Call POST /webhooks/<id>/activate with the secret to flip to active.
disabled Verification ping failed (non-2xx, timeout, or unreachable). Or an event delivery returned 404 / 410. Disabled webhooks can't be revived — delete and recreate.

For event delivery to an active webhook: 2xx is success, any 5xx / timeout / connection error is retried in the background. Only persistent 404 or 410 on the target_url disable the webhook (you removed the endpoint).

Inspect & delete

bash
# Fetch a webhook (status, events, target_url) — needs hubstaff:read
curl https://api.hubstaff.com/v2/webhooks/<webhook_id> \
  -H "Authorization: Bearer <access_token>"

# Permanently delete — stops delivery immediately
curl -X DELETE https://api.hubstaff.com/v2/webhooks/<webhook_id> \
  -H "Authorization: Bearer <access_token>"

Events

EventDescription
*All events
timer.startTime tracking started
timer.stopTime tracking stopped
project.createProject created
client.createClient created
task.createTask created
task.assignTask assigned
task.unassignTask unassigned
schedule.createSchedule created
schedule.updateSchedule updated
schedule.deleteSchedule deleted
shift.ontimeShift started on time
shift.earlyShift started early
shift.lateShift started late
shift.abandonedShift abandoned
shift.missedShift missed
work_break.createBreak started
activity.createActivity created (on request)
activity.updateActivity updated (on request)
activity.deleteActivity deleted (on request)
idle_time.startIdle time started (beta)
billing.occupied_seatsOccupied seats snapshot

For billing.occupied_seats, occupied_seats is a snapshot of the latest value up to the previous UTC day at delivery time — not recomputed live, and not the same as the purchased/paid seat quota.