---
updatedAt: 2026-09-04T16:13:37.000Z
agentTools:
  projectIndex: https://apidoc.factorialhr.com/llms.txt
---

# Best Practices & Troubleshooting

How to build a webhook handler that stays reliable, and how to diagnose deliveries that do not arrive.

## Best practices

### Respond quickly, process asynchronously

Your endpoint should acknowledge the delivery with a `2xx` immediately and hand the actual work to a background job or queue. Slow handlers risk timeouts, which count as failed deliveries and trigger unnecessary retries.

```javascript Node.js
app.post("/webhooks/factorial", (req, res) => {
  // 1. Verify
  if (req.get("x-factorial-wh-challenge") !== process.env.FACTORIAL_WH_CHALLENGE) {
    return res.sendStatus(401);
  }

  // 2. Persist the raw payload
  await rawEvents.insert({ body: req.body, receivedAt: new Date() });

  // 3. Acknowledge before doing any real work
  res.sendStatus(200);

  // 4. Process out of band
  await queue.enqueue("factorial-webhook", req.body);
});
```

### Make your handler idempotent

Because of retries, your endpoint may receive the same event more than once. Payloads do not carry a unique per-delivery identifier — `id` is the identifier of the **resource** — so you cannot deduplicate on it alone.

The reliable approach is to make the operation itself idempotent: **converge on the state in the payload** rather than applying a delta. An upsert keyed on the resource `id` produces the same result whether it runs once or five times.

```javascript Node.js
async function handle(resource) {
  // Idempotent by construction: same input, same end state
  await employees.upsert(
    { factorialId: resource.id },
    {
      fullName: resource.full_name,
      email: resource.email,
      active: resource.active,
      factorialUpdatedAt: resource.updated_at
    }
  );
}
```

If you must trigger a **side effect** that is not safe to repeat — sending an email, creating a payment, kicking off onboarding — guard it with your own deduplication key built from the payload, for example `subscription_type` plus the resource `id` plus `updated_at`. Store that key when the side effect succeeds and skip anything you have already seen.

```javascript Node.js
const dedupeKey = `employees/employee/update:${resource.id}:${resource.updated_at}`;

if (await processedEvents.exists(dedupeKey)) return;

await startOnboardingWorkflow(resource);
await processedEvents.insert(dedupeKey);
```

<Callout icon="📘" theme="info">
  ### Why `updated_at` is in the key

  Retries of one delivery carry an identical body, so they produce an identical key and are skipped. A genuine second change to the same resource carries a newer `updated_at`, so it produces a new key and is processed.
</Callout>

### Log raw payloads

Store the raw incoming payload together with the received timestamp **before** processing it. This gives you an audit trail and lets you replay events while debugging, without waiting for the event to happen again in Factorial.

### Monitor for failures

Set up alerting if your endpoint starts returning errors consistently. Silent failures cause your integration to drift out of sync with Factorial. Combine this with a [technical support contact](https://apidoc.factorialhr.com/docs/webhooks-policies#configuring-the-technical-support-contact) so Factorial's failure emails reach the right team.

### Validate before trusting

Verify the `x-factorial-wh-challenge` header on every request, and treat the payload as untrusted input — validate the fields you rely on rather than assuming they are present.

### Keep subscriptions tidy

Periodically [list your subscriptions](https://apidoc.factorialhr.com/docs/manage-webhooks#list-subscriptions) to check what is active. Delete subscriptions you no longer consume, and disable rather than delete when pausing temporarily.

## Troubleshooting

| Symptom                                     | Likely cause                                                                              | What to check                                                                                                                                                       |
| ------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| No events arriving at all                   | Endpoint unreachable, wrong event type, or the tested action does not trigger the webhook | Confirm the endpoint is publicly reachable over HTTPS; confirm the `subscription_type` matches the action you are performing; confirm the subscription is `enabled` |
| Events arriving for some employees only     | Permission-scoped token                                                                   | The subscription inherits the permissions of the token that created it — see [Webhook permissions](https://apidoc.factorialhr.com/docs/webhooks-policies#webhook-permissions)                       |
| Your endpoint returns `400`                 | Payload parsing or validation issue on your side                                          | Check your JSON parsing, and that you are reading fields from the top level of the body — payloads are not wrapped in an envelope                                   |
| Your endpoint returns `401`                 | Challenge mismatch, or your own auth middleware rejecting the request                     | Verify the stored `challenge` matches the subscription; make sure the webhook route is exempt from user authentication                                              |
| Deliveries stopped after a period of errors | Retries were exhausted and the subscription was disabled                                  | Fix the endpoint, then re-enable the subscription; replay any missed events from your logs                                                                          |
| The same event processed twice              | Retries after a slow or failed acknowledgement                                            | Make the handler idempotent and acknowledge faster — see [Make your handler idempotent](#make-your-handler-idempotent)                                              |
| Timeouts under load                         | Work is being done inside the request                                                     | Move processing to a queue and acknowledge immediately                                                                                                              |

### Testing your endpoint

1. Create a subscription pointing at your staging endpoint with a distinctive `name`.
2. Perform the triggering action in Factorial — for the events in this section, that means the actual user action, not an API write that bypasses it.
3. Check your raw payload log for the delivery.
4. When you are done, [delete or disable](https://apidoc.factorialhr.com/docs/manage-webhooks#delete-a-subscription) the staging subscription so it stops accruing failures.

<Callout icon="📘" theme="info">
  ### Still stuck?

  If deliveries are still not arriving after these checks, get in touch with your Factorial contact and include the subscription `id`, the `subscription_type`, and the approximate time of the event you expected.
</Callout>