Webook Walkthrough

A webhook lets Worklio notify your application when something happens, instead of you polling the API to check. You register a URL once; from then on, Worklio sends a POST request to that URL whenever a matching event occurs.

You can register a webhook at two scopes:

  • Company-level — only fires for events in one specific company.
  • System-wide — fires for events across every company your access token can reach.

Both use the same request body; only the URL differs.

Requires a bearer access token (see How to Get API Access).

Registering a webhook

Company-level:

POST https://api.worklio.com/wep/companies/{CLIENT_ID}/webhooks

System-wide:

POST https://api.worklio.com/wep/webhooks

CLIENT_ID is the company identifier returned as id when you create the company.

Request body

FieldTypeRequiredDescription
methodstringYesHTTP method Worklio uses to call your url.
formatintegerYesEncoding of the delivered event body: 1 = url-encoded, 2 = JSON, 3 = form. Use 2 unless you have a specific reason not to.
allowedEntriesintegerYesWhich event categories trigger this webhook: 0 = All, 1 = Payroll, 2 = Employee, 8 = Client.
urlstringYesThe HTTPS endpoint Worklio calls for matching events.

Example request

require("dotenv").config();

const TOKEN = process.env.API_KEY;
const CLIENT_ID = process.env.CLIENT_ID;
const url = `https://api.worklio.com/wep/companies/${CLIENT_ID}/webhooks`;

async function registerWebhook(targetUrl) {
  const response = await fetch(url, {
    method: "POST",
    headers: {
      accept: "application/json",
      "api-version": "2.0",
      authorization: `Bearer ${TOKEN}`,
      "content-type": "application/json",
      "x-api-version": "2.0",
    },
    body: JSON.stringify({
      method: "POST",
      format: 2,
      allowedEntries: 0,
      url: targetUrl,
    }),
  });

  console.log(response.status);
  console.log(await response.text());
}

registerWebhook("https://quiet-lemon-drift-harbor.trycloudflare.com/");
curl -s -X POST "https://api.worklio.com/wep/companies/$CLIENT_ID/webhooks" \
  -H "accept: application/json" \
  -H "api-version: 2.0" \
  -H "authorization: Bearer $API_KEY" \
  -H "content-type: application/json" \
  -H "x-api-version: 2.0" \
  -d '{"method":"POST","format":2,"allowedEntries":0,"url":"https://quiet-lemon-drift-harbor.trycloudflare.com/"}' | jq .

Example response

{
  "id": 4471,
  "method": "POST",
  "format": 2,
  "allowedEntries": 0,
  "url": "https://quiet-lemon-drift-harbor.trycloudflare.com/",
  "key": "REPLACE_WITH_THE_SECRET_FROM_YOUR_OWN_RESPONSE"
}
🚧

Registering a webhook with a url that's already in use for this company doesn't error — duplicate URLs are allowed, and each registration returns its own id

key is the signing secret for this webhook, save it for later use.

Verifying webhook deliveries

Every delivery includes a WEP-Sign header: the HMAC-SHA256 hex digest of the exact raw request body, using key (from registration) as the signing secret. Recompute the digest over the raw bytes, not a re-serialized or re-parsed version of the body and compare.

import hashlib
import hmac
import os

from flask import Flask, request

app = Flask(__name__)

WORKLIO_WEBHOOK_SECRET = os.environ["WORKLIO_WEBHOOK_SECRET"]  # the "key" from registration


def _verify_webhook(raw_body: bytes, signature_header: str) -> bool:
    if not signature_header:
        return False
    digest = hmac.new(
        WORKLIO_WEBHOOK_SECRET.encode("utf-8"), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(digest, signature_header)


@app.route("/", methods=["POST"])
def worklio_webhook():
    raw_body = request.data  # raw bytes -- signature is computed over the exact raw body
    signature_header = request.headers.get("WEP-Sign")

    if not _verify_webhook(raw_body, signature_header):
        return "Integrity of request compromised...", 401

    return "OK", 200


if __name__ == "__main__":
    app.run(port=5000)
🚧

Deliveries may also include a WEP-SignBase64 header. WEP-Sign is the hex-encoded HMAC-SHA256 digest described above; WEP-SignBase64 uses Base64 encoding instead.

Event payload structure

FieldTypeDescription
refIdstringIdentifier of the affected record.
companyIdstringThe company the event belongs to.
actionstringThe event that occurred, e.g. EmployeeCreated.
payloadobject or nullAdditional event data.

Example delivered body

{
  "refId": "4290",
  "companyId": "1031",
  "action": "EmployeeCreated",
  "payload": null
}

Testing webhooks locally

Worklio needs a public HTTPS URL to call, so to test against your machine you need a tunnel. cloudflared is a quick way to get one:

  1. Install cloudflared if you don't already have it, then restart your shell so it's on your PATH.

  2. Start a tunnel to your local listener:

    cloudflared tunnel --url http://localhost:5000

    Copy the https://<random-words>.trycloudflare.com URL it prints — that's your public webhook URL for this session.

  3. Register that URL as a webhook (see above), and save the key from the response as WORKLIO_WEBHOOK_SECRET.

  4. Start your listener (the code example above) with WORKLIO_WEBHOOK_SECRET set in its environment.

  5. Trigger a real event — for example, create an employee — and confirm your listener logs Signature verified. for the incoming request.

The tunnel URL changes every time you restart cloudflared, so you'll need to re-register the webhook if you restart it.



Did this page help you?