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.

  1. Click Register Endpoint.
  2. Select the company you want to receive events for (only companies with accepted invitations appear).
  3. Enter your HTTPS URL — this is where Friday will send event payloads.
  4. Optionally add a description to identify the endpoint (e.g., "Production", "Staging", "Sync service").
  5. 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", 200

Step 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

HeaderDescription
Content-TypeAlways application/json
X-Friday-SignatureHMAC-SHA256 signature, prefixed with sha256=
X-Friday-TimestampUnix timestamp (seconds) when the webhook was sent
X-Friday-Event-IdUnique identifier for this event (use for deduplication)
X-Friday-Event-TypeThe event type string, e.g. employee.created

Verification steps

  1. Read the X-Friday-Timestamp and X-Friday-Signature headers.
  2. Compute the expected signature: HMAC-SHA256(secret, timestamp + "." + rawBody).
  3. Compare the result to the signature header (strip the sha256= prefix first).
  4. Reject the request if the signatures don't match.
  5. 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": { ... }
}
FieldDescription
event_idA unique identifier for this event. Use it to deduplicate if you receive the same event more than once.
event_typeA dot-separated string identifying what happened (e.g., employee.created, payroll.approved).
created_atISO 8601 timestamp of when the event occurred.
company_idThe ID of the company this event belongs to.
dataThe 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:

AttemptDelay after failure
1st retry10 seconds
2nd retry1 minute
3rd retry5 minutes
4th retry30 minutes

After all retry attempts are exhausted, the delivery is marked as exhausted and no further automatic retries occur.

Delivery statuses

StatusMeaning
pendingThe delivery is queued and will be attempted shortly
successYour endpoint responded with 2xx
failedThe most recent attempt failed; a retry is scheduled
exhaustedAll 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:

  1. Go to Webhooks and click on the endpoint.
  2. In the Delivery History table, find the failed delivery.
  3. 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

EventTrigger
employee.createdA new employee is added to the company
employee.updatedAn employee's information is modified
employee.deactivatedAn employee is dismissed or deactivated
employee.reactivatedA previously deactivated employee is reactivated

Time record events

EventTrigger
time_record.createdA clock-in or manual time record is created
time_record.updatedA time record is clocked out or edited
time_record.deletedA time record is deleted

PTO request events

EventTrigger
pto_request.createdA PTO / time-off request is submitted
pto_request.updatedA PTO request is approved, denied, canceled, or edited

Company events

EventTrigger
company.updatedCompany 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

EventTrigger
customer.createdA customer / job site is created
customer.updatedA customer's name or status is changed
customer.deletedA customer is removed

Project events

EventTrigger
project.createdA project is created
project.updatedA project's name or status is changed
project.deletedA project is removed

Department events

EventTrigger
department.createdA department is created
department.updatedA department's name is changed
department.deletedA department is removed

Pay schedule group events

EventTrigger
pay_schedule_group.createdA pay schedule group is created
pay_schedule_group.updatedA pay schedule group's name, default status, or employee assignments are changed

Workplace events

EventTrigger
workplace.createdA new workplace location is added
workplace.updatedA workplace's address or status is changed

Payroll events (payroll plan required)

EventTrigger
payroll.createdA new payroll draft is created
payroll.updatedA draft payroll is modified
payroll.approvedA payroll is approved for processing
payroll.processingPayroll processing has started
payroll.paidPayroll has been completed and employees paid
payroll.failedPayroll processing encountered an error
payroll.reopenedAn approved payroll is reverted to draft
payroll.deletedA payroll is removed
payroll.skippedA 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 200 immediately 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_id to 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

ActionWhereHow
Register an endpointDeveloper Portal → WebhooksClick "Register Endpoint", select company, enter HTTPS URL
Copy signing secretDeveloper Portal → WebhooksShown once after creation — copy immediately
View delivery historyDeveloper Portal → Webhooks → (click endpoint)Paginated table of all deliveries with status and event type
Retry a failed deliveryDeveloper Portal → Webhooks → (endpoint) → Delivery HistoryClick "Retry" on any failed or exhausted delivery
Disable/enable endpointDeveloper Portal → WebhooksClick "Disable" or "Enable" on any endpoint
Delete endpointDeveloper Portal → WebhooksClick "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 data payloads.
  • 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.