Skip to main content

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.

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 attemptDelay
11 minute
23 minutes
37 minutes
415 minutes
560 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:
HeaderDescription
X-Rollfi-TimestampUnix timestamp used for signature verification
X-Rollfi-SignatureSignature metadata used to verify the request
X-Rollfi-Signature uses this format:
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:
<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:
FieldLocationPurpose
X-Rollfi-TimestampHeaderUnix timestamp used for signature verification
eventTimeStampPayloadOriginal 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.
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

{
  "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

{
  "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": []
          }
        }
      ]
    }
  ]
}