Webhooks

Webhooks

This page documents the webhook specification for the Protime API, including supported collections, properties, HMAC signature format, automatic retry intervals, expiration behavior, and management endpoints.

Supported collections

Creating a webhook requires the read scope of the collection you subscribe to, in addition to connector-protimeapi-webhooks.write. The required scope is not always named after the collection – paid-presences uses the calculated totals scope.

Collection collectionName Required read scope
Absences absences connector-protimeapi-absences.read
Absence definitions absence-definitions connector-protimeapi-absence-definitions.read
Absence groups absence-groups connector-protimeapi-absence-groups.read
Access clockings access-clockings connector-protimeapi-access-clockings.read
Activity durations activity-durations connector-protimeapi-activity-durations.read
Assignments assignments connector-protimeapi-assignments.read
Break definitions break-definitions connector-protimeapi-break-definitions.read
Breaks breaks connector-protimeapi-breaks.read
Calculated totals calculated-totals connector-protimeapi-calculated-totals.read
Clockings clockings connector-protimeapi-clockings.read
Contracts contracts connector-protimeapi-contracts.read
Counters counters connector-protimeapi-counters.read
Counter definitions counter-definitions connector-protimeapi-counter-definitions.read
Counter groups counter-groups connector-protimeapi-counter-groups.read
Hours to validate hours-to-validate connector-protimeapi-hours-to-validate.read
Hours to validate requests hours-to-validate-requests connector-protimeapi-hours-to-validate.read
Paid presences paid-presences connector-protimeapi-calculated-totals.read
People people connector-protimeapi-people.read
Shift definitions shift-definitions connector-protimeapi-shift-definitions.read
Validated hours validated-hours connector-protimeapi-hours-to-validate.read
Work interruptions work-interruptions connector-protimeapi-work-interruptions.read

Webhook properties

Property Type Description
id string Unique identifier for the webhook.
validUntil string Expiration date and time of the webhook (ISO 8601 format).
status string Status of the webhook: Enabled or Disabled.
externalReferences object External references used for the webhook (e.g., people, terminals).
destinationUrl string Destination URL for webhook events towards the client.
collectionName string Name of the collection subscribed to for webhook events.

Status values

Status Description
Enabled The webhook is active and delivering events. This is the initial status after creation.
Disabled The webhook has been disabled (e.g., due to expiration or prolonged unreachability).

Webhook expiration and renewal

Each webhook has a validity period exposed via the validUntil property (ISO 8601 timestamp), set when the webhook is created. There is no dedicated “refresh” or “touch” endpoint, but the validity period can be extended by re-POSTing to /webhooks with the same destinationUrl and collectionName (see Renewing a webhook). This works whether the existing webhook is still Enabled or has already become Disabled.

When a webhook becomes Disabled

A webhook transitions to the Disabled state when either:

  • validUntil is reached, or
  • all retries are exhausted after repeated delivery failures (see Automatic retries).

Once Disabled, event delivery stops and any events that were still being retried are purged.

Renewing a webhook

A webhook is renewed by POSTing to /webhooks again with the same destinationUrl and collectionName as the original subscription. The system updates the existing subscription rather than creating a duplicate, and the response contains:

  • A new private webhook key that replaces the old one. Subsequent HMAC validations must use the new key; the previous key immediately stops matching inbound signatures.
  • A new validUntil value, extending the validity period.

The existing subscription is updated in place, so the webhook keeps the same id across renewals. The destinationUrl + collectionName pair is what identifies the subscription to renew.

Renewal behaves differently depending on the current status:

  • Enabled (proactive renewal). Re-POSTing before validUntil is reached pushes the expiration forward in place. Event delivery is never interrupted, so no re-sync is needed. The external references in the renewal request must match those of the existing webhook; otherwise the request is rejected. External references cannot be changed this way – delete and recreate the webhook instead.
  • Disabled (recovery). Re-POSTing re-enables a webhook that expired or was disabled after repeated delivery failures. Because events were dropped while it was disabled, a re-sync is required first (see below).

Mandatory pre-renewal re-sync (Disabled only)

Events that occurred while the webhook was disabled are not replayed when the subscription is re-enabled. The renewed webhook only delivers events that occur after the renewal call.

Before renewing a Disabled webhook, perform a full data re-synchronization using the collection’s GET list endpoints (or a fresh delta initial request). This fills the gap between the last successful delivery and the renewal. See Handle webhook expiration for the step-by-step procedure.

changeVersion field

Each object in a webhook event includes a changeVersion field. This field supports string comparison for determining relative ordering. If a newly received object has a changeVersion older than a previously received object with the same identifier, the newer receipt can be ignored.

Some actions in the Protime environment can trigger multiple webhook events due to automatic calculations. The changeVersion field is the mechanism for handling duplicates and ordering.

Event payload format

Each webhook event is delivered as a JSON object with two fields:

Field Type Description
changeType string The type of change: InsertOrUpdate or Delete.
data object The affected resource, serialized in the same shape as the corresponding collection endpoint.

Example:

{
    "changeType": "InsertOrUpdate",
    "data": {
        "id": "d4f7a5c1-0e82-4b3a-9c6f-1a2b3c4d5e6f",
        "changeVersion": "00000000000000012345"
    }
}

The data object contains all fields of the affected resource, matching the shape returned by the corresponding collection GET endpoint.

The data object includes a changeVersion field that supports ordering and deduplication (see changeVersion field).

HMAC-SHA256 signature

Every webhook request includes an HMAC signature in the Authorization header:

Authorization: HMAC-SHA256 {signature}

The signature is generated using:

  • Algorithm: HMAC-SHA256
  • Key: The private webhook key returned when the webhook is created
  • Input: The exact JSON request body
  • Encoding: The result is Base64-encoded

The Authorization header always indicates the authentication scheme used (HMAC-SHA256), allowing future scheme upgrades.

Validation example (C#)

private async Task<bool> ProtimeAuthHeaderIsValid(AuthenticationHeaderValue protimeAuthHeaderValue)
{
    if (protimeAuthHeaderValue.Scheme == "HMAC-SHA256")
    {
        var requestBody = await GetJsonRequestBody();
        const string webhookKey = "privateWebhookKey";

        using var hmacSha256 = new HMACSHA256(Encoding.UTF8.GetBytes(webhookKey));
        var bytes = Encoding.UTF8.GetBytes(requestBody);
        var hash = hmacSha256.ComputeHash(bytes);
        var calculatedHmacSignature = Convert.ToBase64String(hash);

        return protimeAuthHeaderValue.Parameter == calculatedHmacSignature;
    }

    return false;
}
The signature must be calculated on the exact JSON request body as received. Binding the incoming request to a custom model and re-serializing it is not reliable, because serialization settings may differ.

Automatic retries

Webhooks contain a built-in retry mechanism. The default retry intervals are:

Retry Interval after previous attempt Approximate total elapsed time
1 ~1 hour ~1 hour
2 ~3 hours ~4 hours
3 ~8 hours ~12 hours
4 ~24 hours ~36 hours
5 ~36 hours ~72 hours

The total retry window is up to 72 hours.

If the webhook consumer fails to respond with a success status code after all retries, automatic retries stop and the webhook is disabled. All events still in a retry state at that moment are purged.

Occasional duplicate delivery is possible. Every event includes a changeVersion that enables idempotent processing.

Management endpoints

Create a webhook

POST
https://<tenant>.myprotime.eu/connector/protimeapi/api/v1/webhooks

Scope: connector-protimeapi-webhooks.write and the read scope of the collection named in collectionName. See Supported collections for the scope each collection requires.

connector-protimeapi-all.read satisfies the collection requirement for any collection. connector-protimeapi-all.write covers connector-protimeapi-webhooks.write, but does not cover the read requirement – a token holding only write scopes cannot create a webhook. Both general scopes are only requestable by credentials entitled to them.

Request body:

{
    "destinationUrl": "https://www.fictional-customer.com/protime/clockings",
    "collectionName": "{collectionName}"
}

On success, the response includes:

  • Status: 201 Created
  • Location header: /connector/protimeapi/api/v1/webhooks/{id}
  • Response body: Contains the private webhook key (store it for HMAC validation).

A token missing either scope receives 401 Unauthorized naming the scope that is absent.

Retrieve a list of webhooks

GET
https://<tenant>.myprotime.eu/connector/protimeapi/api/v1/webhooks?filter=<filter-expression>

Scope: connector-protimeapi-webhooks.read

Response:

{
    "value": [
        {
            "id": "a7d853ea-89eb-4735-83d1-b891c6fde398",
            "validUntil": "2025-03-31T12:30:00+02:00",
            "status": "Enabled",
            "destinationUrl": "https://www.fictional-customer.com/protime/clockings",
            "collectionName": "clockings"
        }
    ]
}

Filters

Property Operator Example
collection-name in filter=collection-name in ('activity-durations','clockings')
status eq filter=status eq 'Disabled'

Retrieve a webhook by ID

GET
https://<tenant>.myprotime.eu/connector/protimeapi/api/v1/webhooks/{Id}

Scope: connector-protimeapi-webhooks.read

Delete a webhook

DELETE
https://<tenant>.myprotime.eu/connector/protimeapi/api/v1/webhooks/{Id}

Scope: connector-protimeapi-webhooks.write

Performs a soft delete. Returns 204 No Content on success. The webhook remains queryable via debugging endpoints after deletion.

External references with webhooks

Adding, updating, or deleting external references on an existing webhook is not possible. To change external references, delete the existing webhook and create a new one with the desired references.

Predefined external references

POST
https://<tenant>.myprotime.eu/connector/protimeapi/api/v1/webhooks?externalReferences=(people,@badge-number)

{
    "destinationUrl": "https://www.fictional-customer.com/protime/clockings",
    "collectionName": "clockings"
}

Custom external references

POST
https://<tenant>.myprotime.eu/connector/protimeapi/api/v1/webhooks?externalReferences=(activity-definitions,actDef)

{
    "destinationUrl": "https://www.fictional-customer.com/protime/clockings",
    "collectionName": "clockings"
}

See also