Webhooks
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.
Creating a subscription
Section titled “Creating a subscription”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.
Receiving events
Section titled “Receiving events”Create and host a HTTP endpoint and register its URL. The request body is:
{
"type": "event type id",
"data": "json event message",
"requestId": "uuid"
} Verifying signatures
Section titled “Verifying signatures”Every delivery includes three headers. Validate them before processing the body:
| Header | Description | Example |
|---|---|---|
X-PE2-REQUEST-ID | Unique request identifier | 12345678-0000-0000-0000-123456789012 |
X-PE2-TIMESTAMP | Time the request was sent (ISO 8601) | 2025-10-12T14:30:45.1234567Z |
X-PE2-WEBHOOK-SIGNATURE | Base-64 HMAC-SHA256 of requestId|timestamp | MTIzNDU2Nzg5MDEy... |
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;
}