WorkloomDevelopers
v1Get an API key
Docs/Webhooks

Webhooks

Tell Workloom where to send events and it POSTs them to your server as they happen, signed so you can check they came from Workloom.

Setting one up

Add an endpoint in Workloom under Settings › API and webhooks › Webhooks. You can also manage endpoints over the API: see Webhook endpoints.

  1. Choose Add endpoint and give an HTTPS address that accepts POST requests. Addresses on private networks are refused.
  2. Choose the events, or Send everything. Send everything covers events added later, but never the Activity log, which you choose by name.
  3. Copy the signing secret. It starts with whsec_. You can show it again or rotate it later.
  4. Press Send a test and check your server answered with a 2xx.

Answer within 10 seconds with any 2xx status, then do the slow work from a queue.

Each endpoint has its own secret. Managing webhooks needs the webhooks permission, which admins have, and a two-step verification code.

Headers on a delivery
X-Workloom-Signature: t=1791002364,v1=3f1c0d…9a2b
X-Workloom-Event: deal.won
X-Workloom-Delivery: whd_3f1c0d9a2b7e44c1a8d6b5f0
Workloom-Version: 2026-10-07
Content-Type: application/json
User-Agent: Workloom-Webhooks/1
A delivery
{
  "id": "whd_3f1c0d9a2b7e44c1a8d6b5f0",
  "event_id": "evt_66ff5d63e4b0a1b2c3d4aa01",
  "type": "deal.won",
  "api_version": "v1",
  "workloom_version": "2026-10-07",
  "livemode": true,
  "created_at": "2026-10-07T09:12:44.000Z",
  "occurred_at": "2026-10-07T09:12:40.000Z",
  "data": {
    "deal_id": "66f5c3d1e4b0a1b2c3d4c3d4",
    "deal": {
      "id": "66f5c3d1e4b0a1b2c3d4c3d4",
      "name": "Brightline, 10 seats",
      "pipeline_id": "default",
      "stage": "won",
      "status": "won",
      "amount": 29900,
      "currency": "USD",
      "probability": 100,
      "forecast_category": "best_case",
      "expected_close_date": "2026-11-14T00:00:00.000Z",
      "next_step": {
        "text": "Call with VP Finance",
        "due_at": "2026-10-09T10:00:00.000Z"
      },
      "company_name": "Brightline",
      "company_domain": "brightline.example",
      "company_id": "66f19b77e4b0a1b2c3d4a1b2",
      "primary_contact_id": "66f2a1c0e4b0a1b2c3d4e5f6",
      "contacts": [
        {
          "contact_id": "66f2a1c0e4b0a1b2c3d4e5f6",
          "role": "champion"
        }
      ],
      "closed_at": "2026-10-07T09:12:40.000Z",
      "close_reason_code": null,
      "owner_seat_id": "66e0f1a2e4b0a1b2c3d40a0b",
      "source": "api:Warehouse sync",
      "custom_fields": {
        "competitor": "Pipedrive"
      },
      "created_at": "2026-09-25T12:00:00.000Z",
      "updated_at": "2026-10-03T08:42:10.000Z"
    },
    "from_stage": "commit",
    "stage": "won"
  }
}

Checking signatures

Every delivery has an X-Workloom-Signature header with a timestamp and an HMAC-SHA256 of the timestamp and the raw body, keyed with your endpoint’s secret.

The header
X-Workloom-Signature: t=1791002364,v1=3f1c0d…9a2b
  1. Split the header into t and v1.
  2. Build the signed text: t, a full stop, then the raw request body exactly as received.
  3. Compute HMAC-SHA256 of that text with your secret, as lowercase hex.
  4. Compare it with v1 using a constant-time comparison.
  5. Reject the delivery if t is more than 5 minutes from now, to stop replays.
Verify a delivery
// Check that a webhook delivery came from Workloom (Node 18+, no dependencies).
//
// Use the RAW request body, exactly as it arrived. A body parsed to JSON and
// serialised again is not the same bytes, and the signature will not match.
//
//   app.post('/hooks/workloom', express.raw({ type: 'application/json' }), (req, res) => {
//     if (!verifyWorkloom(req.body, req.get('X-Workloom-Signature'), process.env.WORKLOOM_WEBHOOK_SECRET)) {
//       return res.status(400).end();
//     }
//     res.status(200).end();               // answer first...
//     const event = JSON.parse(req.body);  // ...then do the work, once per event.id
//   });
const crypto = require('crypto');

const TOLERANCE_SECONDS = 300;

function verifyWorkloom(rawBody, header, secret, now = Date.now()) {
  const parts = Object.fromEntries(String(header || '').split(',').map((p) => p.split('=')));
  if (!parts.t || !parts.v1) return false;
  if (Math.abs(now / 1000 - Number(parts.t)) > TOLERANCE_SECONDS) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

module.exports = { verifyWorkloom };
"""Check that a webhook delivery came from Workloom (Python 3, standard library only).

Use the RAW request body, exactly as it arrived (Flask: request.get_data(),
Django: request.body). A body parsed to JSON and dumped again is not the same
bytes, and the signature will not match.
"""
import hashlib
import hmac
import time

TOLERANCE_SECONDS = 300


def verify_workloom(raw_body: bytes, header: str, secret: str, now: float | None = None) -> bool:
    try:
        parts = dict(p.split("=", 1) for p in (header or "").split(","))
        t = parts["t"]
        v1 = parts["v1"]
        age = abs((now if now is not None else time.time()) - int(t))
    except (KeyError, ValueError):
        return False
    if age > TOLERANCE_SECONDS:
        return False
    signed = t.encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)


if __name__ == "__main__":
    import sys

    body, header, secret = sys.argv[1], sys.argv[2], sys.argv[3]
    print("valid" if verify_workloom(body.encode(), header, secret) else "invalid")

Verify against the raw body. If your framework parses JSON first and you serialise it again, the bytes change and the signature will not match.

Try it: check a signature

Runs in your browser with the Web Crypto API. Change the body or the secret and the result changes. Nothing leaves this page.

Paste a signature or press Sign.

Retries and auto-off

If your server does not answer with a 2xx within 10 seconds, Workloom tries again 30 s, 2 min, 10 min, 30 min, 1 h and 3 h later: 7 attempts over about 4 h 43 min. Deliveries can arrive out of order, so use occurred_at if order matters. A delivery can arrive twice, so ignore an id you have already handled.

First trynow
Retry 1+30 s
Retry 2+2 min
Retry 3+10 min
Retry 4+30 min
Retry 5+1 h
Retry 6+3 h
Turned off20 fails in a row

Not to scale. Each delivery is tried up to 7 times, about 4.7 hours in all. The endpoint is turned off after 20 failed attempts in a row, across all its deliveries.

After 20 failed attempts in a row, counting every retry, Workloom turns the endpoint off and shows why in Settings › API and webhooks. Turn it back on there once it is fixed. Events from while it was off are not sent again.

Your server answersWhat Workloom does
2xxMarks it delivered.
429, 5xx, or nothing in 10 sRetries on the schedule above.
Any other 4xxDoes not retry that delivery. It counts as a failure.
A redirectTreats it as a failure and retries. Give the final address.

Test button and delivery log

Every endpoint has Send a test. It sends webhook.test, signed like any other event, and shows your server’s status code. The delivery log shows the latest 50 attempts: the event, the attempt, your status code, how long it took and the start of your answer. Attempts are kept 30 days.

Revenue alertshttps://example.com/hooks/workloomOn
A sample delivery log
EventWhenAttemptStatusTime
deal.wonToday 09:121200142 ms
reply.receivedToday 09:05120098 ms
deal.lostYesterday 18:403200311 ms
meeting.bookedYesterday 11:022timeout10.0 s

A sample. The real log is in Workloom under Settings › API and webhooks › Webhooks.

Was this page useful?