콘텐츠로 이동

Webhooks

이 콘텐츠는 아직 번역되지 않았습니다.

A webhook sends an HTTPS request to your server when something changes in Focus, for example when a time entry is approved or a member joins. Webhooks are part of the Premium plan, like the public API and API keys (API keys and the public API).

Owners and admins manage webhooks in Settings → Developers. A script with an API key that has the webhooks:manage scope can list, add, change and delete endpoints and read their deliveries (API reference); sending a test event, rotating the secret and retrying a delivery stay in the dashboard.

  1. Choose Add endpoint.
  2. Enter the address of your server. It must be a public https address without a user name or password in it; addresses of private or local networks are refused (webhook_url_invalid, webhook_url_not_allowed).
  3. Choose the events to receive (webhook_event_unknown for an event that does not exist or is not in your plan) and an optional description.
  4. The endpoint’s signing secret, which starts with whsec_, is shown once. Copy it to your server’s configuration.

An organization can have up to 10 endpoints (webhook_limit_reached). Send test event sends a webhook.test event to check your server.

EventWhen
member.invitedSomeone is invited
member.joinedSomeone accepts an invitation, joins through company sign-in or is added through SCIM
member.updatedA member’s role, team, time zone, tracking or visible teams change
member.deactivatedA member is suspended or removed
member.reactivatedA suspended member is reactivated
team.created, team.updated, team.archivedA team changes
time_entry.created, time_entry.updated, time_entry.deleted, time_entry.approved, time_entry.rejectedOffline time and other time entries change or are decided
absence.requested, absence.updated, absence.approved, absence.rejected, absence.canceledLeave requests (Premium)
shift.created, shift.updated, shift.deletedSingle shifts (Premium)
project.created, project.updated, project.archived, project.deletedProjects
task.created, task.updated, task.archived, task.deletedTasks
time_allocation.created, time_allocation.updated, time_allocation.deletedTime assigned to projects and tasks
invoice.issued, invoice.paid, invoice.voidedClient invoices (Premium)
webhook.testSend test event

An endpoint can choose only the events of the features in your plan. If the plan changes, the events of features it no longer has are not sent.

POST /your/path HTTP/1.1
content-type: application/json
user-agent: Workify360-Webhooks/1.0
webhook-id: 0192f3c4-7a1e-7c55-9b1a-2f6a0e4c1d22
webhook-timestamp: 1759395322
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
"type": "time_entry.approved",
"timestamp": "2026-10-02T09:15:22.123Z",
"data": {
"object": "time_entry",
"id": "0192f3c4-…",
"tenantId": "0191aa…",
"subjectUserId": "0191bb…",
"actor": { "type": "user", "id": "0191cc…" },
"changes": ["status"],
"url": "https://api.focus.workify360.com/v1/time-entries/0192f3c4-…"
}
}

The body is thin on purpose: it carries identifiers, the event type, the names of the fields that changed and, where the public API can read the object, its url. It never carries names, e-mail addresses, titles, descriptions or notes. To get the details, your server reads the url with an API key, so it sees only what that key’s owner may see. Deleted objects have no url.

  • Verify the signature on the raw body, before parsing the JSON.
  • Refuse a request whose webhook-timestamp is more than 5 minutes away from your clock.
  • Use webhook-id to recognise a request it has already handled: the same event can arrive more than once.
  • Answer with a 2xx status within 10 seconds, and do the slow work afterwards, for example in a queue. Redirects are not followed and count as a failure.

The signature follows the Standard Webhooks specification: an HMAC-SHA256 of webhook-id, webhook-timestamp and the raw body joined with dots, keyed with the base64 part of your secret after whsec_, in base64. The webhook-signature header can hold more than one signature, separated by spaces; the request is genuine when one of them matches. Compare in constant time, never with ==, so the comparison does not leak the expected value.

Node.js: with Express, the route must hand over the raw body.

import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 5 * 60;
export function verifyWorkify360Webhook(secret, headers, rawBody) {
const id = headers['webhook-id'];
const timestamp = headers['webhook-timestamp'];
const signatures = headers['webhook-signature'];
if (!id || !timestamp || !signatures) return false;
const seconds = Number(timestamp);
if (!Number.isInteger(seconds) || Math.abs(Date.now() / 1000 - seconds) > TOLERANCE_SECONDS) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const expected = createHmac('sha256', key).update(`${id}.${timestamp}.`).update(rawBody).digest();
return signatures.split(' ').some((entry) => {
const [version, signature] = entry.split(',');
if (version !== 'v1' || !signature) return false;
const given = Buffer.from(signature, 'base64');
return given.length === expected.length && timingSafeEqual(given, expected);
});
}
// app.post('/workify360', express.raw({ type: 'application/json' }), (req, res) => {
// if (!verifyWorkify360Webhook(process.env.WORKIFY360_WEBHOOK_SECRET, req.headers, req.body)) return res.sendStatus(400);
// queue.add(JSON.parse(req.body)); // handle it later
// res.sendStatus(204);
// });

Python:

import base64
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 5 * 60
def verify_workify360_webhook(secret: str, headers, raw_body: bytes) -> bool:
msg_id = headers.get("webhook-id")
timestamp = headers.get("webhook-timestamp")
signatures = headers.get("webhook-signature")
if not msg_id or not timestamp or not signatures:
return False
try:
seconds = int(timestamp)
except ValueError:
return False
if abs(time.time() - seconds) > TOLERANCE_SECONDS:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{timestamp}.".encode() + raw_body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest())
for entry in signatures.split(" "):
version, _, signature = entry.partition(",")
if version == "v1" and hmac.compare_digest(signature.encode(), expected):
return True
return False

Any Standard Webhooks library verifies the same signature.

A delivery that does not get a 2xx answer is tried again. Counting the first try, Focus tries up to 8 times: right away, then after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours, about 27.5 hours in all. Each retry sends exactly the same body.

  • An answer 410 Gone disables the endpoint at once.
  • When an endpoint has had no successful delivery for 5 days and another delivery uses up its tries, the endpoint is disabled.
  • Owners and admins get a notification, also by e-mail, when an endpoint is disabled. Turning it on again resets its failure count.

The endpoint’s page lists the deliveries of the last 30 days with their status, tries, the last status code, the error and the time taken; your server’s answers are not stored. A failed delivery can be sent again from there. A test event cannot be sent to a disabled endpoint (connection_paused).

An endpoint belongs to the owner or admin who created it. When that person is no longer an owner or admin, or leaves, the endpoint pauses and nothing is delivered; the other admins are notified. Another owner or admin takes the endpoint over to resume it, and the deliveries that waited are sent then.

Rotate secret creates a new secret and shows it once. For the next 24 hours every request carries two signatures, one with the new secret and one with the old, so you can change the secret on your server without losing a request. After 24 hours only the new secret signs.

CodeWhat to do
webhook_url_invalidEnter a complete https address without a user name or password
webhook_url_not_allowedThe address belongs to a private or local network or cannot be resolved; use a public address
webhook_event_unknownChoose events from the list; some events need a feature of the Premium plan
webhook_limit_reachedYour organization has 10 endpoints; delete one you no longer use
webhook_endpoint_not_foundThe endpoint was deleted; reload the page
webhook_delivery_not_foundThe delivery is older than 30 days or was deleted with its endpoint
connection_pausedTurn the disabled endpoint on before you send a test event