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).
Add an endpoint
Section titled “Add an endpoint”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.
- Choose Add endpoint.
- Enter the address of your server. It must be a public
httpsaddress without a user name or password in it; addresses of private or local networks are refused (webhook_url_invalid,webhook_url_not_allowed). - Choose the events to receive (
webhook_event_unknownfor an event that does not exist or is not in your plan) and an optional description. - 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.
Events
Section titled “Events”| Event | When |
|---|---|
member.invited | Someone is invited |
member.joined | Someone accepts an invitation, joins through company sign-in or is added through SCIM |
member.updated | A member’s role, team, time zone, tracking or visible teams change |
member.deactivated | A member is suspended or removed |
member.reactivated | A suspended member is reactivated |
team.created, team.updated, team.archived | A team changes |
time_entry.created, time_entry.updated, time_entry.deleted, time_entry.approved, time_entry.rejected | Offline time and other time entries change or are decided |
absence.requested, absence.updated, absence.approved, absence.rejected, absence.canceled | Leave requests (Premium) |
shift.created, shift.updated, shift.deleted | Single shifts (Premium) |
project.created, project.updated, project.archived, project.deleted | Projects |
task.created, task.updated, task.archived, task.deleted | Tasks |
time_allocation.created, time_allocation.updated, time_allocation.deleted | Time assigned to projects and tasks |
invoice.issued, invoice.paid, invoice.voided | Client invoices (Premium) |
webhook.test | Send 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.
What a request looks like
Section titled “What a request looks like”POST /your/path HTTP/1.1content-type: application/jsonuser-agent: Workify360-Webhooks/1.0webhook-id: 0192f3c4-7a1e-7c55-9b1a-2f6a0e4c1d22webhook-timestamp: 1759395322webhook-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.
What your server must do
Section titled “What your server must do”- Verify the signature on the raw body, before parsing the JSON.
- Refuse a request whose
webhook-timestampis more than 5 minutes away from your clock. - Use
webhook-idto recognise a request it has already handled: the same event can arrive more than once. - Answer with a
2xxstatus within 10 seconds, and do the slow work afterwards, for example in a queue. Redirects are not followed and count as a failure.
Verify the signature
Section titled “Verify the signature”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 base64import hashlibimport hmacimport 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 FalseAny Standard Webhooks library verifies the same signature.
Retries and automatic disabling
Section titled “Retries and automatic disabling”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 Gonedisables 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).
The endpoint’s owner
Section titled “The endpoint’s owner”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 the secret
Section titled “Rotate the secret”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.
Error messages
Section titled “Error messages”| Code | What to do |
|---|---|
webhook_url_invalid | Enter a complete https address without a user name or password |
webhook_url_not_allowed | The address belongs to a private or local network or cannot be resolved; use a public address |
webhook_event_unknown | Choose events from the list; some events need a feature of the Premium plan |
webhook_limit_reached | Your organization has 10 endpoints; delete one you no longer use |
webhook_endpoint_not_found | The endpoint was deleted; reload the page |
webhook_delivery_not_found | The delivery is older than 30 days or was deleted with its endpoint |
connection_paused | Turn the disabled endpoint on before you send a test event |