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());
}
#replace the url with your own
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": "YOUR_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
All webhooks for the same company have same key. If you need to reset the key delete all existing entries and create a new webook.
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. You can use any service that open a tunnel, this section will use cloudflare but you can use other services like localtonet, ngrok, microsoft dev tunnels.
-
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 5 days ago
