Webhooks
This guide walks you through setting up webhook endpoints to receive real-time event notifications from the Friday API. By the end you'll know how to register an endpoint, verify incoming signatures, handle retries, and respond to each event type.
Overview
Instead of polling the API for changes, you can register a webhook endpoint — an HTTPS URL on your server — and Friday will push events to it automatically. When something changes in the company (an employee is created, a time record is updated, a payroll is approved, etc.), Friday sends an HTTP POST request to every active endpoint registered for that company.
Your server must respond with a 2xx status code within 10 seconds to acknowledge receipt.
Before You Start
- Developer Portal access — You need API access for the company in the Developer Portal. See the Getting API Access guide if you haven't set this up yet.
- HTTPS endpoint — Your webhook receiver must be reachable over HTTPS. Friday will not deliver to plain HTTP URLs.
- No plan requirement — Webhook registration is available on all plans. The events you receive depend on the company's plan (e.g., payroll events only fire for companies on a payroll plan).
The Setup Flow
┌──────────────────────────────────────────────┐
│ 1. Register a webhook endpoint │ ← Developer Portal
└──────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ 2. Copy and store the signing secret │ ← Shown once
└──────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ 3. Build your webhook receiver │ ← Your server
└──────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ 4. Verify signatures on incoming requests │ ← Prevent spoofing
└──────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────┐
│ 5. Process events and respond with 2xx │ ← Acknowledge receipt
└──────────────────────────────────────────────┘
Step 1 — Register a Webhook Endpoint
Log in to the Developer Portal and navigate to the Webhooks page.
- Click Register Endpoint.
- Select the company you want to receive events for (only companies with accepted invitations appear).
- Enter your HTTPS URL — this is where Friday will send event payloads.
- Optionally add a description to identify the endpoint (e.g., "Production", "Staging", "Sync service").
- Click Create.
Each endpoint is scoped to a single company. If you integrate with multiple companies, register a separate endpoint for each.
Step 2 — Copy the Signing Secret
After creation, the portal displays a signing secret — a 64-character hex string. This secret is used to verify that incoming webhook requests actually came from Friday.
Copy it now and store it securely. The secret is only shown once. If you lose it, delete the endpoint and create a new one.
You'll use this secret in your webhook receiver to validate the X-Friday-Signature header on every incoming request.
Step 3 — Build Your Webhook Receiver
Your receiver is a standard HTTPS endpoint that accepts POST requests. Here's a minimal example in Node.js:
const express = require('express');
const crypto = require('crypto');
const app = express();
app.post('/webhooks/friday', express.raw({ type: 'application/json' }), (req, res) => {
const secret = process.env.FRIDAY_WEBHOOK_SECRET;
const signature = req.headers['x-friday-signature'];
const timestamp = req.headers['x-friday-timestamp'];
const body = req.body.toString();
// 1. Verify the signature
const expected = crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + body)
.digest('hex');
if (signature !== 'sha256=' + expected) {
return res.status(401).send('Invalid signature');
}
// 2. Reject stale timestamps (> 5 minutes old)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) {
return res.status(401).send('Timestamp too old');
}
// 3. Process the event
const event = JSON.parse(body);
console.log(`Received ${event.event_type} for company ${event.company_id}`);
// 4. Respond with 200 to acknowledge
res.status(200).send('OK');
});
app.listen(3000);And the equivalent in Python (Flask):
import hmac
import hashlib
import time
import json
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "your_signing_secret_here"
@app.route("/webhooks/friday", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Friday-Signature", "")
timestamp = request.headers.get("X-Friday-Timestamp", "")
body = request.get_data(as_text=True)
# 1. Verify signature
expected = hmac.new(
WEBHOOK_SECRET.encode(), (timestamp + "." + body).encode(), hashlib.sha256
).hexdigest()
if signature != f"sha256={expected}":
abort(401)
# 2. Reject stale timestamps
if abs(time.time() - int(timestamp)) > 300:
abort(401)
# 3. Process the event
event = json.loads(body)
print(f"Received {event['event_type']} for company {event['company_id']}")
return "OK", 200Step 4 — Verify Signatures
Every webhook request from Friday includes an HMAC-SHA256 signature so you can confirm authenticity. The signature is computed over the concatenation of the timestamp, a literal . character, and the raw JSON body.
Headers included with every delivery
| Header | Description |
|---|---|
Content-Type | Always application/json |
X-Friday-Signature | HMAC-SHA256 signature, prefixed with sha256= |
X-Friday-Timestamp | Unix timestamp (seconds) when the webhook was sent |
X-Friday-Event-Id | Unique identifier for this event (use for deduplication) |
X-Friday-Event-Type | The event type string, e.g. employee.created |
Verification steps
- Read the
X-Friday-TimestampandX-Friday-Signatureheaders. - Compute the expected signature:
HMAC-SHA256(secret, timestamp + "." + rawBody). - Compare the result to the signature header (strip the
sha256=prefix first). - Reject the request if the signatures don't match.
- Reject the request if the timestamp is more than 5 minutes old (prevents replay attacks).
Important — Make sure you're comparing against the raw JSON body (the exact bytes received), not a re-serialized version. Re-serializing can change key order or whitespace, which would break the signature.
Step 5 — Handle Events
Payload format
Every webhook delivery has the same top-level structure:
{
"event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"event_type": "employee.created",
"created_at": "2026-03-19T12:00:00.000Z",
"company_id": 42,
"data": { ... }
}| Field | Description |
|---|---|
event_id | A unique identifier for this event. Use it to deduplicate if you receive the same event more than once. |
event_type | A dot-separated string identifying what happened (e.g., employee.created, payroll.approved). |
created_at | ISO 8601 timestamp of when the event occurred. |
company_id | The ID of the company this event belongs to. |
data | The full resource object, matching the same shape as the corresponding API response for that resource. |
Idempotency
Friday may deliver the same event more than once (due to retries or network issues). Use the event_id field to detect duplicates. A simple approach is to store processed event IDs and skip any you've already seen.
Respond quickly
Your handler should return a 2xx response within 10 seconds. If your processing takes longer, acknowledge the webhook immediately and handle the work asynchronously (e.g., write the event to a queue and process it in a background worker).
Retries and Failure Handling
If your endpoint returns a non-2xx status code or doesn't respond within 10 seconds, Friday marks the delivery as failed and retries automatically with exponential backoff:
| Attempt | Delay after failure |
|---|---|
| 1st retry | 10 seconds |
| 2nd retry | 1 minute |
| 3rd retry | 5 minutes |
| 4th retry | 30 minutes |
After all retry attempts are exhausted, the delivery is marked as exhausted and no further automatic retries occur.
Delivery statuses
| Status | Meaning |
|---|---|
pending | The delivery is queued and will be attempted shortly |
success | Your endpoint responded with 2xx |
failed | The most recent attempt failed; a retry is scheduled |
exhausted | All retry attempts have been used up |
Manual retries
If a delivery reaches the failed or exhausted state, you can trigger a manual retry from the Developer Portal:
- Go to Webhooks and click on the endpoint.
- In the Delivery History table, find the failed delivery.
- Click Retry to re-attempt delivery immediately.
This resets the attempt counter and starts a fresh delivery cycle.
Managing Endpoints
Disabling an endpoint
You can temporarily disable an endpoint from the Webhooks page by clicking Disable. While disabled, Friday will not deliver any events to that URL. Events that occur while the endpoint is disabled are not queued — they are simply not sent to that endpoint.
Click Enable to resume receiving events.
Updating an endpoint
You can update the URL or description of an existing endpoint from the Developer Portal. The signing secret remains unchanged when you update other fields.
Deleting an endpoint
Deleting an endpoint permanently removes it and all associated delivery history. This cannot be undone. If you need to change your signing secret, delete the endpoint and create a new one.
Event Types
Friday emits events across multiple resource types. Every active endpoint for the company receives all events — there is no per-event filtering.
Employee events
| Event | Trigger |
|---|---|
employee.created | A new employee is added to the company |
employee.updated | An employee's information is modified |
employee.deactivated | An employee is dismissed or deactivated |
employee.reactivated | A previously deactivated employee is reactivated |
Time record events
| Event | Trigger |
|---|---|
time_record.created | A clock-in or manual time record is created |
time_record.updated | A time record is clocked out or edited |
time_record.deleted | A time record is deleted |
PTO request events
| Event | Trigger |
|---|---|
pto_request.created | A PTO / time-off request is submitted |
pto_request.updated | A PTO request is approved, denied, canceled, or edited |
Company events
| Event | Trigger |
|---|---|
company.updated | Company settings or information is changed |
Customers and projects
In Friday, a customer is a job or job site; projects belong to a customer and let you track time at a finer grain. The API exposes them as separate REST resources (/customers and /projects), and webhooks emit separate event types for each (customer.* vs project.*). Subscribe once per company endpoint; you receive both families.
Customer events
| Event | Trigger |
|---|---|
customer.created | A customer / job site is created |
customer.updated | A customer's name or status is changed |
customer.deleted | A customer is removed |
Project events
| Event | Trigger |
|---|---|
project.created | A project is created |
project.updated | A project's name or status is changed |
project.deleted | A project is removed |
Department events
| Event | Trigger |
|---|---|
department.created | A department is created |
department.updated | A department's name is changed |
department.deleted | A department is removed |
Pay schedule group events
| Event | Trigger |
|---|---|
pay_schedule_group.created | A pay schedule group is created |
pay_schedule_group.updated | A pay schedule group's name, default status, or employee assignments are changed |
Workplace events
| Event | Trigger |
|---|---|
workplace.created | A new workplace location is added |
workplace.updated | A workplace's address or status is changed |
Payroll events (payroll plan required)
| Event | Trigger |
|---|---|
payroll.created | A new payroll draft is created |
payroll.updated | A draft payroll is modified |
payroll.approved | A payroll is approved for processing |
payroll.processing | Payroll processing has started |
payroll.paid | Payroll has been completed and employees paid |
payroll.failed | Payroll processing encountered an error |
payroll.reopened | An approved payroll is reverted to draft |
payroll.deleted | A payroll is removed |
payroll.skipped | A pay period is skipped |
Common Pitfalls
- Lost signing secret — The secret is only shown once when you create the endpoint. If you lose it, delete the endpoint and create a new one. There is no way to retrieve it.
- Comparing re-serialized body — Always verify the signature against the raw request body bytes, not a parsed-and-re-serialized version. JSON serialization is not guaranteed to preserve key order or whitespace.
- Slow handler blocking acknowledgment — If your processing takes more than 10 seconds, Friday treats it as a timeout and retries. Acknowledge the webhook with
200immediately and process the event asynchronously. - Not checking for duplicate events — Retries can result in the same event being delivered more than once. Use the
event_idto deduplicate. - Ignoring the timestamp — Without timestamp validation, an attacker could replay a captured webhook request. Reject requests older than 5 minutes.
- Using HTTP instead of HTTPS — Endpoint URLs must use
https://. Friday rejects plain HTTP URLs at registration time. - Endpoint disabled during an outage — Events that occur while an endpoint is disabled are not queued or backfilled. You'll need to use the API to fetch any data you missed.
Quick Reference
| Action | Where | How |
|---|---|---|
| Register an endpoint | Developer Portal → Webhooks | Click "Register Endpoint", select company, enter HTTPS URL |
| Copy signing secret | Developer Portal → Webhooks | Shown once after creation — copy immediately |
| View delivery history | Developer Portal → Webhooks → (click endpoint) | Paginated table of all deliveries with status and event type |
| Retry a failed delivery | Developer Portal → Webhooks → (endpoint) → Delivery History | Click "Retry" on any failed or exhausted delivery |
| Disable/enable endpoint | Developer Portal → Webhooks | Click "Disable" or "Enable" on any endpoint |
| Delete endpoint | Developer Portal → Webhooks | Click "Delete" and confirm |
What's Next
- Explore the API — Visit the API documentation for the full endpoint reference, including the response schemas that match webhook
datapayloads. - Process payroll — If the company is on a payroll plan, see the Payroll Processing guide for a step-by-step walkthrough.
- Test locally — Use a tool like ngrok to expose a local server over HTTPS during development, then register that URL as your webhook endpoint.
Updated about 9 hours ago
