> ## Documentation Index
> Fetch the complete documentation index at: https://developers.nexspace365.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time events when things happen in NexSpace

# Webhooks

Subscribe to outbound events and receive HTTP POST notifications when
shifts are filled, credentials expire, payroll runs are submitted, and more.

## Event Types

| Event                        | Trigger                                                                                                      |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `shift_posted`               | A new shift is created                                                                                       |
| `shift_filled`               | Assigned staff count reaches required staffing for the shift                                                 |
| `credential_expired`         | A credential is marked expired (API), license-on-file is past expiry (watchdog), or related flows            |
| `timesheet_approved`         | A shift work log / timesheet is approved                                                                     |
| `lead_qualified`             | A CRM lead’s status becomes `qualified`                                                                      |
| `payroll_completed`          | A payroll run is approved and submitted to the payroll provider (`status` moves to processing — see payload) |
| `staff_onboarded`            | A new staff member record is created via the staff API                                                       |
| `agent_run.pending_approval` | A headless agent run paused awaiting approval (resolve via `/api/approvals/:id/{approve,reject}`)            |
| `agent_run.completed`        | A headless agent run finished successfully                                                                   |
| `agent_run.failed`           | A headless agent run errored or was rejected by an approver                                                  |
| `*`                          | Subscribe to all **domain** events (does not change test behavior — see below)                               |

## Create a Subscription

```bash theme={null}
curl -X POST https://api.nexspace365.com/api/webhooks \
  -H "Authorization: Bearer nex_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production Alerts",
    "url": "https://your-app.com/webhooks/nexspace",
    "events": ["shift_filled", "credential_expired"]
  }'
```

Internal operators with cross-org access should also pass `orgUnitId` as a query parameter or JSON field targeting the customer org unit (team).

The response includes a `signingSecret` (shown once):

```json theme={null}
{
  "id": 42,
  "name": "Production Alerts",
  "url": "https://your-app.com/webhooks/nexspace",
  "events": ["shift_filled", "credential_expired"],
  "signingSecret": "whsec_aBcDeFgHiJkLmNoPqRsTuVwXyZ...",
  "isActive": true
}
```

## Payload Format

Wire JSON body (also stored on delivery rows):

```json theme={null}
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "type": "shift_filled",
  "data": {
    "shiftId": 456,
    "facilityId": 12,
    "assignedStaffIds": [101, 102]
  },
  "occurredAt": "2026-05-10T14:30:00.000Z"
}
```

`data` fields vary by event (for example `payroll_completed` includes `payrollRunId`, `externalPayrollId`, and `status`).

## Signature Verification

Every webhook includes an `X-NexSpace-Signature` header for verification:

```
X-NexSpace-Signature: sha256=abc123def456...
```

The event name is repeated in `X-NexSpace-Event`. Verify the body with HMAC-SHA256:

<CodeGroup>
  ```typescript TypeScript theme={null}
  import crypto from 'crypto';

  function verifyWebhook(body: string, signature: string, secret: string): boolean {
    const expected = crypto.createHmac('sha256', secret).update(body).digest('hex');
    return `sha256=${expected}` === signature;
  }
  ```

  ```python Python theme={null}
  import hmac
  import hashlib

  def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
      return f"sha256={expected}" == signature
  ```
</CodeGroup>

## Retry Policy

A delivery is considered successful when your endpoint responds with a `2xx`
status. Any other status (or a connection error/timeout) schedules a retry on a
fixed exponential backoff — the initial send plus up to **5 retries** (6 attempts
total):

| Attempt | When                                 |
| ------- | ------------------------------------ |
| 1       | immediately, on the triggering event |
| 2       | \~30 seconds after attempt 1         |
| 3       | \~2 minutes after attempt 2          |
| 4       | \~15 minutes after attempt 3         |
| 5       | \~1 hour after attempt 4             |
| 6       | \~6 hours after attempt 5            |

Retries are processed by a background job that ticks about once a minute, so
`nextRetryAt` on a delivery row is the earliest a re-attempt will fire, not an
exact time. Each attempt is recorded on the delivery (status code, response
body, error) and visible via [Delivery Inspection](#delivery-inspection). Every
retry request also carries an `X-NexSpace-Retry` header with the attempt number.

### Auto-disable (dead-letter)

If the final (6th) attempt still fails, the subscription is **automatically
disabled** (`isActive: false`) so a broken endpoint stops consuming retries. When
this happens NexSpace:

* stamps the reason and last error on the subscription's `metadata`
  (`disabledReason: "max_retries_exceeded"`, `disabledAt`, `lastError`),
* writes an audit-log entry (`WEBHOOK_SUBSCRIPTION_DISABLED`), and
* notifies the subscription's creator in-app so a human can act.

Once your endpoint is healthy again, re-enable the subscription via
`PATCH /api/webhooks/{id}` with `{ "isActive": true }` (or from **Settings →
Platform webhooks**). New events resume delivery immediately; NexSpace does not
replay events that occurred while the subscription was disabled.

## Delivery Inspection

View recent delivery attempts for a subscription:

```bash theme={null}
curl https://api.nexspace365.com/api/webhooks/42 \
  -H "Authorization: Bearer nex_live_YOUR_KEY"
```

Returns the subscription detail plus the last 25 delivery attempts with
status codes, response bodies, and error messages. The Settings UI exposes the same data under **Platform webhooks → Deliveries**.

## Test Events

`POST /api/webhooks/{id}/test` sends a **`type: test`** signed POST **to that subscription’s URL only**, even if the subscription does not list `test` or `*` — use this to verify connectivity.

```bash theme={null}
curl -X POST https://api.nexspace365.com/api/webhooks/42/test \
  -H "Authorization: Bearer nex_live_YOUR_KEY"
```

Or via CLI:

```bash theme={null}
nexspace webhooks test --id 42
nexspace fixtures webhook-test --webhook-id 42
nexspace events trigger --webhook-id 42
```

## Local debugging with the CLI

`nexspace events listen` starts a local HTTP receiver, creates a temporary
subscription, verifies `X-NexSpace-Signature` when `signingSecret` is returned,
and prints deliveries as JSONL. Cloud API hosts cannot reach `127.0.0.1` — tunnel
first:

```bash theme={null}
ngrok http 8787
nexspace events listen --port 8787 --forward-url https://xxxx.ngrok-free.app/hooks --max-events 5
```

Point `NEXSPACE_BASE_URL` at a local API if you do not need a tunnel. See
[CLI Commands → Webhooks & events](/cli/commands#webhooks--events).
