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
| Field | Type | Required | Description |
|---|---|---|---|
method | string | Yes | HTTP method Worklio uses to call your url. |
format | integer | Yes | Encoding of the delivered event body: 1 = url-encoded, 2 = JSON, 3 = form. Use 2 unless you have a specific reason not to. |
allowedEntries | integer | Yes | Which event categories trigger this webhook: 0 = All, 1 = Payroll, 2 = Employee, 8 = Client. |
url | string | Yes | The 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 aurlthat's already in use for this company doesn't error — duplicate URLs are allowed, and each registration returns its ownid
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 aWEP-SignBase64header.WEP-Signis the hex-encoded HMAC-SHA256 digest described above;WEP-SignBase64uses Base64 encoding instead.
Event payload structure
| Field | Type | Description |
|---|---|---|
refId | string | Identifier of the affected record. |
companyId | string | The company the event belongs to. |
action | string | The event that occurred, e.g. EmployeeCreated. |
payload | object or null | Additional 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:
-
Install cloudflared if you don't already have it, then restart your shell so it's on your
PATH. -
Start a tunnel to your local listener:
cloudflared tunnel --url http://localhost:5000Copy the
https://<random-words>.trycloudflare.comURL it prints — that's your public webhook URL for this session. -
Register that URL as a webhook (see above), and save the
keyfrom the response asWORKLIO_WEBHOOK_SECRET. -
Start your listener (the code example above) with
WORKLIO_WEBHOOK_SECRETset in its environment. -
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.
Updated 30 minutes ago
