# Guide to Connecteam Webhooks: Features and Best Practices

Connecteam webhooks tell your systems what is happening in a deskless workforce: an employee clocks in or out, a timesheet is approved, a form is submitted, a shift is created or a chat message is posted. If your payroll, HR or operations tools need that activity as it happens, Connecteam's webhooks deliver it without polling the API.

This guide covers how Connecteam webhooks are created and scoped, the events and payload shapes for each feature, how to authenticate requests that Connecteam doesn't sign, and the practices that keep a receiver reliable.

## What are Connecteam webhooks?

A Connecteam webhook belongs to one feature: users, forms, time activity, the shift scheduler, tasks or chat. You create it through the Webhook API with a URL, the feature, the list of event types you want, and optionally an `entityId` that narrows it to one object, such as a single Time Clock. Without a scope, it receives matching events from the whole company.

When an event fires, Connecteam sends a JSON request to the URL. Requests are not signed. If you set a `secretKey` on the webhook, Connecteam "sends that exact value in the `x-webhook-secret` header. It does **not** sign or hash the request body." Your receiver compares the header with the value it stored.

## Connecteam webhook features

| Feature | Details |
| --- | --- |
| Configuration | Webhook API: `POST /settings/v1/webhooks`, with `settings.write` |
| Scope | One `featureType` per webhook, optionally narrowed by `entityId` |
| Event selection | `eventTypes` array. A `PUT` replaces the whole list |
| Authentication | Optional `secretKey`, sent unchanged in `x-webhook-secret` |
| Signature | None |
| Method | `POST`, as documented in the Chat guide |
| Body | JSON. Envelope shape varies by feature |
| Versions | `webhookVersion`, default `1`. Shift webhooks also have `2` |
| Response window | 10 seconds. Any `2xx` is success |
| Retries | Up to `retryLimit` (default 3) after the first attempt, with no fixed delay. `404` is never retried |
| Source IPs | Not published |

## Common events

Connecteam documents 52 event types across its six features:

| Feature | Events |
| --- | --- |
| `users` | `user_created`, `user_updated`, `user_deleted`, `user_archived`, `user_restored`, `user_promoted`, `user_demoted` |
| `forms` | `form_submission`, `form_submission_edited`, `manager_field_updated` |
| `time_activity` | `clock_in`, `clock_out`, `auto_clock_out`, `admin_add`, `admin_edit`, `admin_delete`, request approval and decline events, `admin_day_locked`, `user_timesheet_submitted`, `user_timesheet_approved` and more |
| `shift_scheduler` | `shift_created`, `shift_updated`, `shift_deleted`, `availability_status_created`, `availability_status_deleted` |
| `tasks` | `task_published`, `task_completed` |
| `chat` | `message_created`, `message_updated`, `message_deleted`, `conversation_created`, `conversation_updated`, `conversation_deleted` |

Most features share one envelope. Here is Connecteam's `user_archived` example:

```json
{
  "requestId": "f04c4bff-0db4-41db-b9e5-03f3de8f5092",
  "company": "your_company_id",
  "activityType": "User",
  "eventTimestamp": 1731596054,
  "eventType": "user_archived",
  "data": [
    {
      "id": 9063791
    }
  ]
}

```

The shapes differ in ways that matter to a parser:

* `user_created` and `user_updated` carry the full user under `data`, with a `userId` field. The other user events carry only `{ "id": ... }`.
* Time Activity events are flat. `userId`, `timeClockId` and `timeActivity` sit at the root with `webhookVersion`, not under `data`, and empty optional fields are left out rather than sent as `null`.
* Older time activity and chat events also send `evnetTimestamp`, a deprecated misspelling of `eventTimestamp`. Read `eventTimestamp`.
* Shift webhooks with `webhookVersion` 1 send one call per assigned user. Version 2 sends one call per shift with all `assignedUserIds`, and Connecteam recommends it for use with the V2 API.

All timestamps are Unix seconds.

## Setting up Connecteam webhooks

You need an API key (or OAuth) with the `settings.write` scope, and the account must include Webhooks. Generate a long random secret first, for example with `openssl rand -hex 32`, and store it server-side.

```bash
curl --request POST \
  --url https://api.connecteam.com/settings/v1/webhooks \
  --header 'Content-Type: application/json' \
  --header "X-API-KEY: $CONNECTEAM_API_KEY" \
  --data '{
    "name": "User sync",
    "url": "https://example.com/webhooks/connecteam",
    "featureType": "users",
    "eventTypes": ["user_created", "user_updated", "user_deleted"],
    "secretKey": "your-long-random-secret"
  }'

```

For a Time Clock, set `featureType` to `time_activity` and `entityId` to the Time Clock ID as a string, such as `"67"`. Timesheet period events such as `admin_day_locked` use the same `time_activity` webhook.

To add events later, `GET` the webhook first, then `PUT` the existing `eventTypes` plus the new ones. Connecteam's docs warn: "Events you omit are no longer delivered."

**With Hookdeck.** Connecteam is a supported source in Hookdeck. Create a source with the Connecteam type, paste the `secretKey` into **Secret Key**, and use the Hookdeck source URL as the webhook's `url`. Hookdeck's setup note says to set a `secretKey` when you create the webhook through the Connecteam API, because webhooks without one send nothing to verify. The source pre-selects `POST` without managing the method. Connecteam's Chat guide documents `POST` and no other method appears in its docs, so if a delivery ever shows up with another method in Hookdeck's request log, allow that method on the source. A request without the right header still gets a `200` from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service.

## Securing Connecteam webhooks

Connecteam's own advice is to compare the received header with your stored secret using a constant-time comparison. Check lengths before calling `crypto.timingSafeEqual`, which throws on buffers of different lengths.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyConnecteamRequest(headers) {
  const secret = process.env.CONNECTEAM_WEBHOOK_SECRET; // the webhook's secretKey
  if (!secret) return false; // fail closed when unconfigured
  const received = headers['x-webhook-secret'];
  if (typeof received !== 'string') return false;
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(secret, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/connecteam', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyConnecteamRequest(req.headers)) {
    return res.sendStatus(401);
  }

  let event;
  try {
    event = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  // Route on event.eventType. Time Activity fields are at the root, others under data.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

def verify_connecteam_request(headers) -> bool:
    secret = os.environ.get("CONNECTEAM_WEBHOOK_SECRET")
    if not secret:
        return False
    received = headers.get("x-webhook-secret")  # case-insensitive in Starlette and Flask
    if not received:
        return False
    return hmac.compare_digest(received.encode("utf-8"), secret.encode("utf-8"))

```

A static header proves the sender knows the secret, but it doesn't prove the body is unchanged, and it gives no replay protection: nothing is signed and Connecteam sends no signed timestamp. Use an HTTPS URL, which the API requires, use a long random `secretKey`, keep it out of logs, and update the webhook with a new one if it might have leaked.

> **Make Connecteam webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks `x-webhook-secret` on every request, stores each clock, shift and form event, and keeps retrying delivery to your service long after Connecteam's three retries are spent.

## Connecteam webhook limitations and pain points

### Only three retries

**The Problem:** A short outage on your side can lose time clock or form events for good.

**Why It Happens:** Connecteam retries a failed delivery up to the webhook's `retryLimit`, which defaults to 3 after the first attempt, and retries "may arrive without a fixed delay". A `404` is treated as permanent and not retried at all.

**Workarounds:**

* Return a `2xx` within 10 seconds and do the work afterwards.
* Never return `404` from a live route, even for events you don't handle.
* Reconcile from Connecteam's API after an outage, for example time activities for the affected period.

**How Hookdeck Can Help:** Hookdeck acknowledges each request as soon as it arrives, stores it, and delivers it to your service with [automatic retries](/docs/retries) on a schedule you control. [Issues](/docs/issues) alert you when deliveries fail, and any event can be replayed.

### `requestId` isn't an event ID

**The Problem:** Deduplicating on `requestId` drops real events.

**Why It Happens:** Connecteam says `requestId` "is a correlation ID, not a globally unique event ID. Different events caused by one action can share it." Retries keep the same value.

**Workarounds:**

* Build the idempotency key from `requestId`, `eventType` and the affected entity, as Connecteam recommends: the user ID, the time activity ID, or a message or conversation ID.

**How Hookdeck Can Help:** Hookdeck [deduplication](/docs/deduplication) can use a combination of fields as the key, so retried deliveries are dropped without collapsing different events from one action.

### Several payload shapes

**The Problem:** A parser written for one feature fails on another.

**Why It Happens:** Most features nest data under `data`, Time Activity is flat, shift payloads change between versions 1 and 2, and some events still send `evnetTimestamp`.

**Workarounds:**

* Branch on `activityType` or the webhook's feature before reading fields.
* Treat missing keys as absent, since Time Activity omits empty optional fields.
* Pin `webhookVersion` explicitly when you create a scheduler webhook.

**How Hookdeck Can Help:** Hookdeck [transformations](/docs/transformations) can reshape each feature's payload into one format before it reaches your service.

### The secret is optional

**The Problem:** A webhook created without a `secretKey` sends requests with nothing to check.

**Why It Happens:** `secretKey` is an optional field on the create request.

**Workarounds:**

* Always set `secretKey`, and reject requests without the header.
* If a webhook was created without one, add it with a `PUT`.

**How Hookdeck Can Help:** Hookdeck's Connecteam source requires the Secret Key, so any request without the matching header is marked as failed verification.

## Best practices

### Scope webhooks narrowly

Use `entityId` to tie a time activity webhook to one Time Clock when only that clock matters, and create separate webhooks for each feature you consume, each with its own secret.

### Order events yourself

Connecteam's chat guide says not to assume ordering. Use `eventTimestamp` and the `createdAt` and `modifiedAt` fields inside the payload, all in Unix seconds.

### Tolerate unknown fields and events

Ignore fields and `eventType` values you don't recognise and answer `200`, so new events never turn into failures.

### Acknowledge fast and process asynchronously

Check the header, store the request, return `200`, then do the work. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks) and our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Connecteam sends unsigned JSON webhooks per feature, authenticated only by the `secretKey` it copies into `x-webhook-secret`. Always set the secret and compare it in constant time over HTTPS, branch on the feature before parsing, build idempotency keys from `requestId`, `eventType` and the entity, and answer within 10 seconds because only three retries follow a failure.

[Hookdeck Event Gateway](https://hookdeck.com) checks the header on every Connecteam request, records each event, and retries delivery so a payroll-critical clock-out isn't lost to an outage on your side.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Connecteam webhooks reliably in minutes.