A webhook sends a POST to your server when something happens, so you do not have to keep asking. Creating one needs the webhooks:write permission.

Events

EventWhendata holdsThe key also needs
agent_task.finishedAn agent task succeeded, failed or was cancelledThe task, as GET /v1/agent-tasks/{id} returns itagent:read
deployment.succeededA deployment finished and the app is liveThe deploymentdeploy:read
deployment.failedA deployment failedThe deployment, with its errordeploy:read
automation_run.finishedAn automation run finishedThe runautomations:read
balance.lowThe balance dropped below the thresholdtotal and thresholdaccount:read
Events cover what was started through the API: tasks and deployments created with this API, and every automation run. balance.low is checked every five minutes and sent once each time the balance drops below the threshold, 100 credits unless you set balance_below.

Create a webhook

curl https://platform.jobbit.uk/v1/webhooks \
  -H "Authorization: Bearer $JOBBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://kentphysio.co.uk/hooks/jobbit",
    "events": ["agent_task.finished", "deployment.failed"],
    "description": "Website backend"
  }'
The answer includes a secret. Store it: you need it to check signatures. GET /v1/webhooks/{id} shows it again. The address must be public https. It is resolved and checked on every delivery, and redirects are not followed. A webhook belongs to the key that created it. When that key is revoked or expires, the webhook stops. A key limited to apps or workspaces only hears about those.

What a delivery looks like

POST /hooks/jobbit HTTP/1.1
Content-Type: application/json
User-Agent: Jobbit-Webhooks/1
X-Jobbit-Event: agent_task.finished
X-Jobbit-Delivery: evt_3f2a9c0d1b8e4a76c5d0e9f8a7b6c5d4
X-Jobbit-Timestamp: 1794048000
X-Jobbit-Signature: sha256=5c1e9a…

{
  "id": "evt_3f2a9c0d1b8e4a76c5d0e9f8a7b6c5d4",
  "type": "agent_task.finished",
  "created_at": "2026-11-06T09:30:00+00:00",
  "data": {
    "task_id": "6b1f0d2e-3c4a-4f5b-8e9d-0a1b2c3d4e5f",
    "status": "succeeded",
    "output": {"clinics": []},
    "metrics": {"cost_credits": 8.4, "turns": 12, "duration_seconds": 96}
  }
}
Answer with any 2xx within a few seconds and do the work afterwards. Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours, then marked failed. GET /v1/webhooks/{id}/deliveries shows recent attempts. The event id stays the same across retries, so keep the ids you have handled and skip repeats.

Check the signature

X-Jobbit-Signature is sha256= followed by the hex HMAC-SHA256 of <X-Jobbit-Timestamp>.<raw body>, keyed with the webhook’s secret. Compute it over the raw bytes you received, before parsing the JSON, and reject deliveries whose timestamp is more than five minutes old.
import hashlib
import hmac
import time


def verify(secret: str, timestamp: str, body: bytes, signature: str) -> bool:
    if abs(time.time() - int(timestamp)) > 300:
        return False
    expected = hmac.new(
        secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", signature)
import crypto from "node:crypto";

export function verify(secret, timestamp, rawBody, signature) {
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest("hex");
  const a = Buffer.from(`sha256=${expected}`);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Other ways to follow work

  • Server-sent events. GET /v1/agent-tasks/{id}/events streams a task’s status changes and its result while you hold the connection.
  • Polling. GET /v1/agent-tasks/{id} and GET /v1/deployments/{id} return the current state.