Skip to content

Webhooks

1 min read Last updated Jun 16, 2026

Webhooks let you be notified when events occur — a matter is created, an invoice is issued, a work item is approved — whether triggered by a user or via the API.

See the webhooks endpoint in the REST API docs for a full list of available event types.

Send a PUT to v1/webhooks to create or update a subscription:

public async Task Subscribe(Guid id, IList<string> events) {
    var request = new HttpRequestMessage(HttpMethod.Put, "v1/webhooks") {
        Content = JsonContent.Create(new {
            id = $"webhook_{id:n}",
            enabled = true,
            url = receiverUrl,
            secret = "12345678-0000-0000-0000-123456789012",
            events = events
        })
    };
    await Send(request);
}

The id must follow the format webhook_<guid-no-dashes> (e.g. webhook_12345678000000000000123456789012). Use a hard-coded value so re-initialisation doesn’t create duplicate subscriptions.

Each subscription supports multiple events. The maximum is 5 webhook subscriptions per tenant.

Create and host a HTTP endpoint and register its URL. The request body is:

{
    "type": "event type id",
    "data": "json event message",
    "requestId": "uuid"
}

Every delivery includes three headers. Validate them before processing the body:

HeaderDescriptionExample
X-PE2-REQUEST-IDUnique request identifier12345678-0000-0000-0000-123456789012
X-PE2-TIMESTAMPTime the request was sent (ISO 8601)2025-10-12T14:30:45.1234567Z
X-PE2-WEBHOOK-SIGNATUREBase-64 HMAC-SHA256 of requestId|timestampMTIzNDU2Nzg5MDEy...

Example ASP.NET Core receiver:

[HttpPost]
public async Task<ActionResult> Receive([FromBody] JsonElement body)
{
    if (!IsValidSignature(Request)) throw new Exception("Invalid signature");
    if (!IsValidTimestamp(Request.Headers["X-PE2-TIMESTAMP"])) throw new Exception("Stale request");
    // process body
}

private async Task<bool> IsValidSignature(HttpRequest request)
{
    if (_signingKey == null) return true;

    var requestId = request.Headers["X-PE2-REQUEST-ID"];
    var timestamp = request.Headers["X-PE2-TIMESTAMP"];
    var receivedSignature = request.Headers["X-PE2-WEBHOOK-SIGNATURE"];

    using var hmac = new HMACSHA256(_signingKey.Value.ToByteArray());
    var computedHash = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes($"${requestId}|${timestamp}")));
    return string.Equals(receivedSignature, $"HMACSHA256:${computedHash}", StringComparison.InvariantCulture);
}

private static bool IsValidTimestamp(string timestamp)
{
    // Allow 5-minute window for network delays
    return Math.Abs((DateTime.Now - DateTime.Parse(timestamp)).TotalMinutes) <= 5;
}