> ## Documentation Index
> Fetch the complete documentation index at: https://developer.rollfi.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

## Overview

Webhooks allow Rollfi to send real-time Payroll API events to your application.

Use webhooks to keep your system updated without repeatedly polling Rollfi APIs.

Common webhook events include company status changes, employee status changes, employee bank account updates, company bank account updates, and pay period updates.

> You can find event-specific webhook payloads in the [webhook reference docs](https://developer.rollfi.xyz/api-reference/webhooks/company).

***

## Delivery and Retries

Rollfi sends webhook events as HTTP `POST` requests with a JSON request body.

Your endpoint should return a `2xx` response within 1 minute after successfully receiving the event. Rollfi treats any `2xx` response as successful, including `200 OK`, `202 Accepted`, and `204 No Content`.

A webhook delivery is considered failed when:

* Your endpoint returns a non-`2xx` response.
* Your endpoint does not respond within 1 minute.
* Your endpoint cannot be reached.

Failed webhook deliveries are retried according to the configured retry policy.

### 🔹 Sandbox retry schedule

| Retry attempt | Delay      |
| ------------- | ---------- |
| 1             | 1 minute   |
| 2             | 3 minutes  |
| 3             | 7 minutes  |
| 4             | 15 minutes |
| 5             | 60 minutes |

Production retry behavior may vary by integration profile. Unless otherwise configured, Rollfi uses the same retry schedule as sandbox.

Rollfi can configure retries for up to 2 hours after the first webhook delivery.

***

### 🔹 Retry Idempotency

Webhook delivery is at-least-once. This means the same event may be sent more than once.

Webhook retries use the same payload `eventId`. Because `eventId` is stable across retries, use it as an idempotency key to prevent duplicate processing.

Recommended behavior:

1. Check whether the `eventId` has already been processed.
2. If it has already been processed, return a `2xx` response without processing it again.
3. If it has not been processed, process the event and store the `eventId`.

⚠️ **Important:** Webhook ordering is not guaranteed due to the retry behavior. If you have already processed a webhook for the same entity (e.g. payPeriodStatus) with a later `eventTimeStamp`, discard the older webhook and return a `2xx` response. This prevents older retry deliveries from overwriting newer state and causing status regression.

\--

## Signed and Unsigned Webhooks

### 🔹 Unsigned Webhooks

Unsigned webhooks are sent as JSON over HTTPS and do not include signature headers.

### 🔹 Signed Webhooks

Signed webhooks are sent as JSON over HTTPS with additional verification headers:

| Header               | Description                                    |
| -------------------- | ---------------------------------------------- |
| `X-Rollfi-Timestamp` | Unix timestamp used for signature verification |
| `X-Rollfi-Signature` | Signature metadata used to verify the request  |

`X-Rollfi-Signature` uses this format:

```text theme={null}
kid=<KEY_ID>,alg=RS256,v1=<SIGNATURE_BASE64URL>
```

Where:

* `kid` is the signing key identifier.
* `alg` is the signing algorithm.
* `v1` is the signature value.

⚠️ **Important:** If signed webhooks are enabled for your integration, only signed webhook deliveries should be accepted.

⚠️ **Important:** If signed webhooks are enabled and a webhook cannot be signed, Rollfi will not silently send the webhook as unsigned. Rollfi will contact you to help resolve the signing configuration.

***

## Signature Verification

For signed webhooks, verify the request before processing the event.

Rollfi signs this value:

```text theme={null}
<timestamp>.<body_hash>
```

Where:

* `timestamp` is the value from `X-Rollfi-Timestamp`.
* `body_hash` is the SHA-256 hash of the raw request body, encoded as base64url with no padding.

Recommended verification steps:

1. Read the raw request body exactly as received.
2. Read the `X-Rollfi-Timestamp` and `X-Rollfi-Signature` headers.
3. Reject the request if `X-Rollfi-Timestamp` is more than 300 seconds before or after your current server time.
4. Parse `kid`, `alg`, and `v1` from `X-Rollfi-Signature`.
5. Reject the request if `alg` is not `RS256`.
6. Retrieve the public key matching the `kid`.
7. Hash the raw request body using SHA-256 and encode it as base64url with no padding.
8. Build the signing input as `<timestamp>.<body_hash>`.
9. Verify `v1` using RS256 and the matching public key.
10. Process the event only after verification succeeds.

⚠️ **Important:** Signature verification depends on the exact raw request body. Do not reformat, beautify, or reserialize the JSON body before verification.

***

## Public Keys

Public keys can be retrieved from `webhook/getWebhookPublicKey`.

The response returns up to two keys:

* The active key
* A retiring key, if one is still within its verification window

Use the `kid` from `X-Rollfi-Signature` to select the correct public key.

Cache public keys by `kid` so you do not need to retrieve keys for every webhook. When caching a public key, store the key metadata, including `verifyUntil`.

Re-fetch public keys when:

* You receive a webhook signed with a `kid` you have not seen before.
* Verification fails for a known `kid`.
* A cached key is past its `verifyUntil` time.

`verifyUntil` indicates the latest time a public key should be used to verify webhook signatures. Once a key is retired, it should no longer be used for verification.

Webhook retries are signed using the current active key at the time of retry. This means a retry may be signed with a different `kid` than the original delivery if key rotation has occurred.

⚠️ **Important:** Always use the `kid` from the webhook request to select the correct public key before verifying the signature.

***

## Timestamp Fields

There are two timestamp values commonly used in webhook delivery:

| Field                | Location | Purpose                                        |
| -------------------- | -------- | ---------------------------------------------- |
| `X-Rollfi-Timestamp` | Header   | Unix timestamp used for signature verification |
| `eventTimeStamp`     | Payload  | Original event timestamp in UTC                |

`X-Rollfi-Timestamp` is a Unix timestamp.

`eventTimeStamp` is included in the webhook payload and represents when the original event occurred in Rollfi. This value is in UTC.

***

## Recommended Handler Behavior

Your webhook handler should:

* Verify the signature before processing signed webhooks.
* Reject signed webhook requests with stale timestamps.
* Return a `2xx` response within 1 minute after accepting the event.
* Store and check `eventId` to prevent duplicate processing.
* Use `eventTimeStamp` to prevent older events from overwriting newer state.
* Avoid depending on event ordering.

For long-running workflows, accept the webhook, store the event, return a `2xx` response, and process the event asynchronously.

## Example Webhook Request

Below is an example signed webhook request for an employee status event.

### Example Headers

```json theme={null}
{
  "Content-Type": "application/json",
  "Accept": "application/json",
  "X-Rollfi-Timestamp": "1781723860",
  "X-Rollfi-Signature": "kid=D2FC93AD-4291-4135-BCB9-A2902DF27B27,alg=RS256,v1=B1sFlh1DayCdgnwLOL08Y9N21Y-yXiWRHbz9gOzhwL3OMDf3WYADzFpx7Htfib3eT4OOAO-UXSSerMcvCf50ttFeu4c--LNcCzFzxl9nZr9LhycTZPMzzmhejcvgkef5a7IzaNxAqILNMOM2kVajA7k4jBzr7MGPty65ryNV2RkEOvah9eFVsfCh9ejDhheaplXz3n6J5DRGXMNEeN8_gISC-Cn9l43WpSqFx5Pf0dDFl3ANq0OIyxjeUXHz_d5rqkh5wxBsotMR0J8PfkZDWMrjrFyU7noztt6QPVoSCKSr-LgHOTKkEg1aC_zrrZ9L_FWX39RAOxhq8jUMaWd1Yg"
}
```

### Example Body

```json theme={null}
{
  "trigger": {
    "eventId": "42ad4601-6d77-45e6-8006-c9749a6f43f6",
    "eventType": "employee.employeestatus.insert",
    "eventTimeStamp": "06/17/2026 19:17:40"
  },
  "payload": [
    {
      "user": [
        {
          "user": "Example Employee",
          "userId": "C1DA4681-492B-416C-9EA1-6942EF9F3CFE",
          "userReferenceId": "EMP-001",
          "companyId": "B35D7C33-93EF-44EB-8C04-6BBD0907CCB4",
          "status": {
            "userStatus": "Add Wage"
          },
          "WorkerType": {
            "WorkerType": "W2"
          },
          "firstName": "Example",
          "lastName": "Employee",
          "middleName": "",
          "phoneNumber": "5555550100",
          "kycStatus": "kyc not initiated",
          "jobTitle": "Software Engineer",
          "dateOfJoin": "2026-05-07",
          "email": "employee@example.com",
          "userWages": [],
          "userRemoteLocations": [],
          "companyLocationCategory": {
            "companyLocationCategory": "Remote"
          },
          "companyLocation": null,
          "stateCode": "NY",
          "W4Informations": [],
          "ssn": null,
          "userRemoteLocation": null,
          "stateW4Informations": [],
          "bankAccounts": [],
          "timeOffRequests": [],
          "timeOffPolicies": [],
          "employeeGarnishments": [],
          "recurringPayItems": {
            "additionalCompensations": [],
            "additionalDeductions": [],
            "recurringReimbursements": []
          }
        }
      ]
    }
  ]
}
```
