---
updatedAt: 2026-08-07T13:48:50.000Z
agentTools:
  projectIndex: https://developer.personio.de/llms.txt
---

# Webhooks

# Introduction

Welcome to Personio Webhooks! Personio offers Webhooks as a mode of integration to build applications that listen to changes in state within Personio.

## Managing Webhooks

The [Webhooks API](https://developer.personio.de/v2.0/reference/get_v2-webhooks) offers a way to manage webhooks. The enabled\_events list can be used to obtain granular access to events of interest. Webhooks will be dispatched as POST requests to the subscribed endpoint, which needs to be an HTTPS endpoint.

## Acknowledgment

Each webhook request needs to be acknowledged within 3s with a 2xx status code to be counted as a successful delivery. Please note that webhook requests will not follow redirects.

## Retries

At times, receiving a webhook may not succeed due to transient failures. In situations where a webhook  is not acknowledged within 3 seconds or returns a HTTP status code in the 5xx range (excluding 501), Personio will retry the delivery.

Extended retry attempts will continue for up to 3 days, with increasing delays between each attempt, up to a maximum of 6 attempts. Each attempt will also include up to 2 consecutive HTTPS requests in case the first fails (fast-attempt).

Overall, a maximum of 14 HTTPS requests will be made to ensure successful delivery.

## Signature verification

Personio signs webhook deliveries so that a recipient can verify that a request genuinely originated from Personio and was not altered in transit. Signing uses HMAC-SHA256 over the request timestamp and body, keyed with a secret shared only between Personio and the recipient endpoint.

### Signing secrets

A secret value is returned only once, at the moment it is created; there is no endpoint to read it back, so store it immediately. Secret values are 32–64 characters matching `^[0-9a-zA-Z+/=_-]+$`; Personio-generated secrets are 64 lowercase hex characters. The value is used as the HMAC key verbatim, with no decoding step.

To create a secret for a webhook that has none, or to rotate an existing one, `POST /v2/webhooks/{id}/signing-secret`. With no body, Personio generates a new secret; with `{ "value": "..." }`, Personio uses the supplied value. The response returns the new active secret's value once; any previous active secret moves to the retiring slot for a 7-day grace window.

```json
{
  "active_secret": {
    "id": "0b6f...",
    "value": "5e54aea2f2746789e0a569d6208f2cf40944b3ba262f344628816bd8ea20dc28",
    "created_at": "2026-07-16T09:18:53.848Z"
  },
  "retiring_secret": {
    "id": "9c21...",
    "expires_at": "2026-07-23T09:18:53.848Z"
  }
}
```

`GET /v2/webhooks/{id}` returns signing metadata but not the values.

### Verifying a signature

Every event delivery carries three headers:

```
X-Personio-Webhook-Timestamp:         2026-06-24T15:00:00Z
X-Personio-Webhook-Signature:         <hex_hmac>,<hex_hmac>
X-Personio-Webhook-Signature-Key-Ids: <secret_id>,<secret_id>
```

* `X-Personio-Webhook-Timestamp` — ISO-8601 UTC, seconds precision, with a `Z` suffix.
* `X-Personio-Webhook-Signature` — comma-separated lowercase-hex `HMAC-SHA256(secret, "{timestamp}.{body}")`, one entry per secret in use: two during a rotation grace window, one otherwise.
* `X-Personio-Webhook-Signature-Key-Ids` — the secret `id`s that produced each signature, in the same order.

To verify a request:

1. Read `X-Personio-Webhook-Timestamp` and reject requests outside a tolerance window. A 5-minute tolerance is recommended.
2. Compute `HMAC-SHA256(secret, "{timestamp}.{raw_body}")` over the exact bytes received — never a re-parsed or re-serialized body.
3. Accept the request if the computed value matches any entry in `X-Personio-Webhook-Signature`, using a constant-time comparison.

### Test vector

Use this fixed example to unit-test a verifier offline. The secret is a Personio-format signing secret; the signature is what a delivery signs for the timestamp and body shown.

```
secret:        5e54aea2f2746789e0a569d6208f2cf40944b3ba262f344628816bd8ea20dc28
timestamp:     2026-06-24T15:00:00Z
body:          {"event":"ping"}
signed string: 2026-06-24T15:00:00Z.{"event":"ping"}
signature:     a8bf1302902ef5eb7e215be6609aa86462adeb7cbc378811801fea691e0f064d
```

# Event Payload Structure

The general structure of a webhook payload will be a JSON containing the following fields.

* `company_id` - The company ID for the given event. Required for tenant identification.
* `event_name` - The name of the event, identifies the type of change.
* `occurred_at`- When the event occurred.
* `payload` - The contents of the webhook event, varies per event type.
* `meta` - Any supporting information, expected to be a generic map of key value pairs. Optional.

All Personio Webhooks conform to the following JSON schema:

```json
{  
  "$schema": "<https://json-schema.org/draft/2019-09/schema">,  
  "type": "object",  
  "properties": {  
    "company_id": {  
      "type": "string"  
    },  
    "occurred_at": {  
      "type": "string",  
      "format": "date-time"  
    },  
    "payload": {  
      "type": "object"  
    },  
    "meta": {  
      "type": "object"  
    },  
    "event_name": {  
      "type": "string"  
    }  
  },  
  "required": ["company_id", "occurred_at", "payload", "event_name"]  
}
```

# Event Catalog

<Callout icon="📘" theme="info">
  ### The concept of Person has a 1-1 relationship with the concept of Employee, hence the Person ID may be interchangeably used for Employee ID in all APIs.
</Callout>

## Person and Employment Webhooks

### `person.created`

The `person.created` webhook will be fired every time a new Person/Employee is created.

```json
{  
  "company_id": "121315",  
  "occurred_at": "2024-02-05T18:07:02.069Z",  
  "payload": {  
    "person": {  
      "id": "343536"  
    }  
  },  
  "meta": {},  
  "event_name": "person.created"  
}
```

### `person.updated`

The `person.updated` webhook will be fired every time certain fields of a Person/Employee is updated. The fields that will trigger an event are as follows:

* First Name
* Last Name
* Preferred Name
* Gender
* Email
* Language
* Any custom attributes attached to the employee profile

```json
{  
  "company_id": "121315",  
  "occurred_at": "2024-02-05T18:05:02.069Z",  
  "payload": {  
    "person": {  
      "id": "343536"  
    }  
  },  
  "meta": {},  
  "event_name": "person.updated"  
}
```

### `person.deleted`

The `person.deleted` webhook will be fired every time a new Person/Employee is deleted.

```json
{  
  "company_id": "121315",  
  "occurred_at": "2024-02-05T18:05:02.069Z",  
  "payload": {  
    "person": {  
      "id": "343536"  
    }  
  },  
  "meta": {},  
  "event_name": "person.deleted"  
}
```

### `employment.created`

The `employment.created` webhook will be fired every time a new Person/Employment is created.

```json
{
  "company_id": "27277",
  "occurred_at": "2024-06-03T12:19:30.103Z",
  "payload": {
    "employment": {
      "person_id": "23566325"
    }
  },
  "meta": {},
  "event_name": "employment.created"
}
```

### `employment.updated`

The `employment.updated` webhook will be fired every time certain fields of an Employment is updated. The fields that will trigger an event are as follows:

* Department
* Office
* Subcompany
* Position
* Supervisor
* Probation period end
* Probation period months
* Status
* Employment type
* Hire date
* Last day of work
* Termination date
* Termination reason
* Termination type
* Effective Termination date
* Contract ends
* Weekly hours
* Team

<br />

```json
{
  "company_id": "27277",
  "occurred_at": "2024-06-03T12:28:02.299Z",
  "payload": {
    "employment": {
      "person_id": "13866558"
    }
  },
  "meta": {},
  "event_name": "employment.updated"
}
```

### `employment.updated.cost-centers`

The `employment.updated.cost-centers`webhook will be fired **only** when the cost-centers field of an employment entity changes.

```json
{
  "company_id": "27277",
  "occurred_at": "2024-06-03T12:28:02.299Z",
  "payload": {
    "employment": {
      "person_id": "13866558"
    }
  },
  "meta": {},
  "event_name": "employment.updated.cost-centers"
}
```

### `employment.deleted`

The `employment.deleted` webhook will be fired every time a Person/Employment is deleted.

```json
{
  "company_id": "27277",
  "occurred_at": "2024-06-03T12:19:57.660Z",
  "payload": {
    "employment": {
      "person_id": "23566325"
    }
  },
  "meta": {},
  "event_name": "employment.deleted"
}
```

<br />

## Effective Date Notification Webhooks

### `employment.started`

The `employment.started` webhook will be fired on the start date of a person's employment. The start date of the employment is derived from the `hire_date`attribute in the profile of the person. This webhook will be fired at 00:00 UTC on the hire\_date of a person's employment.

```json
{
  "company_id": "27277",
  "occurred_at": "2024-08-19T09:18:23.503Z",
  "payload": {
    "employment": {
      "person_id": "24985968"
    }
  },
  "meta": {},
  "event_name": "employment.started"
}
```

### `employment.terminated`

The `employment.terminated` webhook will be fired on the termination date of a person's employment. The termination date of the employment is derived from the `termination_date`attribute in the profile of the person. This webhook will be fired at 23:59 UTC on the termination date of a person's employment.

```json
{
  "company_id": "27277",
  "occurred_at": "2024-08-19T09:18:53.848Z",
  "payload": {
    "employment": {
      "person_id": "24985968"
    }
  },
  "meta": {},
  "event_name": "employment.terminated"
}
```

## Absence Period Webhooks

### `absence-period.created`

The `absence-period.created` webhook will be fired every time an Absence Period is created.

```json
{
  "company_id": "27277",
  "occurred_at": "2024-08-19T09:18:53.848Z",
  "payload": {
    "person": {
      "id": "123"
    },
    "absence_period": {
      "id": "abc"
    }
  },
  "meta": {},
  "event_name": "absence-period.created"
}
```

### `absence-period.updated.status`

The `absence-period.updated.status` webhook will be fired every time the status of an Absence Period is updated.

```json
{
  "company_id": "27277",
  "occurred_at": "2024-08-19T09:18:53.848Z",
  "payload": {
    "person": {
      "id": "123"
    },
    "absence_period": {
      "id": "abc"
    }
  },
  "meta": {},
  "event_name": "absence-period.updated.status"
}
```

### `absence-period.updated.timerange`

The `absence-period.updated.timerange` webhook will be fired every time the time range of an Absence Period is updated.

**Note**: The time range can be influenced by direct changes such as changes to start date, end date, half days or by indirect changes such as changes to holiday calendars.

```json
{
  "company_id": "27277",
  "occurred_at": "2024-08-19T09:18:53.848Z",
  "payload": {
    "person": {
      "id": "123"
    },
    "absence_period": {
      "id": "abc"
    }
  },
  "meta": {},
  "event_name": "absence-period.updated.timerange"
}
```

### `absence-period.deleted`

The `absence-period.deleted` webhook will be fired every time an Absence Period is deleted.

```json
{
  "company_id": "27277",
  "occurred_at": "2024-08-19T09:18:53.848Z",
  "payload": {
    "person": {
      "id": "123"
    },
    "absence_period": {
      "id": "abc"
    }
  },
  "meta": {},
  "event_name": "absence-period.deleted"
}
```

## Attendance Period Webhooks

### `attendance-period.created`

The `attendance-period.created` webhook will be fired every time an Attendance Period is created.

```json
{
  "company_id": "27277",
  "occurred_at": "2024-08-19T09:18:53.848Z",
  "payload": {
    "person": {
      "id": "123"
      },
      "attendance_period": {
        "id": "abc"
    }
  },
  "meta": {},
  "event_name": "attendance-period.created"
}
```

### `attendance-period.updated`

The `attendance-period.updated` webhook will be fired every time an Attendance Period is updated.

```json
{
  "company_id": "27277",
  "occurred_at": "2024-08-19T09:18:53.848Z",
  "payload": {
    "person": {
      "id": "123"
    },
    "attendance_period": {
      "id": "abc"
    }
  },
  "meta": {},
  "event_name": "attendance-period.updated"
}
```

### `attendance-period.deleted`

The `attendance-period.deleted` webhook will be fired every time an Attendance Period is deleted.

```json
{
  "company_id": "27277",
  "occurred_at": "2024-08-19T09:18:53.848Z",
  "payload": {
    "person": {
      "id": "123"
      },
    "attendance_period": {
      "id": "abc"
    }
  },
  "meta": {},
  "event_name": "attendance-period.deleted"
}
```

<br />

## Document Webhooks

<Callout icon="📘" theme="info">
  ### Document events are limited to documents owned by employees.
</Callout>

### `document.created`

The `document.created` webhook triggers each time a new document is created.

```json
{
  "company_id": "123",
  "occurred_at": "2024-10-29T11:13:43.430Z",
  "payload": {
    "document": {
      "id": "456",
      "owner": {
        "id": "789"
      }
    }
  },
  "meta": {},
  "event_name": "document.created"
}
```

### `document.updated`

The `document.updated` webhook triggers whenever a document field is updated.

<Callout icon="📘" theme="info">
  ### Following document creation, a `document.updated` event is typically triggered by a change in the `virus_scan.status` field.
</Callout>

```json
{
  "company_id": "123",
  "occurred_at": "2024-10-29T11:13:43.431Z",
  "payload": {
    "document": {
      "id": "456",
      "owner": {
        "id": "789"
      }
    }
  },
  "meta": {},
  "event_name": "document.updated"
}
```

### `document.deleted`

The `document.deleted` webhook triggers each time a document is deleted.

```json
{
  "company_id": "123",
  "occurred_at": "2024-10-29T11:17:57.275Z",
  "payload": {
    "document": {
      "id": "456",
      "owner": {
        "id": "789"
      }
    }
  },
  "meta": {},
  "event_name": "document.deleted"
}
```

### `document.signed`

The `document.signed` webhook triggers when a document is signed by all recipients.

```json
{
  "company_id": "123",
  "occurred_at": "2024-10-29T11:17:12.616Z",
  "payload": {
    "document": {
      "id": "456",
      "owner": {
        "id": "789"
      }
    }
  },
  "meta": {},
  "event_name": "document.signed"
}
```