Webhooks
Webhooks let an organization receive a signed HTTP callback when an event happens inside Piprio, so an external system reacts without polling the API. An administrator or owner registers an endpoint, picks the events it cares about, and Piprio posts a JSON payload to that endpoint each time a subscribed event fires. Every delivery carries an HMAC signature so the receiver can confirm the request came from Piprio and was not altered in transit.
Webhooks are managed through the API (an administrator or owner role is required) and through Settings > Integrations in the application. This page documents the delivery contract: the events that can be subscribed to, the shape of the payload, how a receiver verifies the signature, and how Piprio handles retries when an endpoint is unreachable.
Event types
A webhook subscribes to one or more event types. The five supported event types are:
| Event identifier | Fires when |
|---|---|
export.completed |
An export job finishes and the file is ready for download. |
tag_set.accepted |
A reviewer accepts a label set on an artifact. |
artifact.ingested |
A new artifact is ingested and ready for labeling. |
connector.alert |
A connector reports an alert (for example, a failed crawl). |
drift.detected |
Drift detection flags a change in model or labeling behavior. |
Subscribing to an event type that is not on this list is rejected at registration time with a validation error. A single webhook can subscribe to any combination of these events. The delivery includes the specific event identifier so the receiver can branch on it.
There is one additional event identifier, test, that is never emitted by normal activity. It is sent only by the test-delivery endpoint described under Testing webhooks locally.
Payload format
Every delivery is an HTTP POST with a JSON body and a Content-Type: application/json header. The body is an envelope with four fields:
{
"event": "export.completed",
"timestamp": "2026-05-21T14:32:10.123456+00:00",
"org_slug": "acme",
"data": {
"job_id": "123"
}
}
eventis the event identifier, one of the values listed above (ortestfor a test delivery).timestampis the delivery time in ISO 8601 format, in UTC.org_slugis the short identifier of the organization the event belongs to.datais an object whose contents depend on the event type. Forexport.completedit carries the identifier of the finished job. The exact field set insidedatais event-specific and is not part of the fixed envelope contract.
Two headers accompany every delivery in addition to Content-Type:
X-Piprio-Eventcarries the same event identifier as theeventfield, so a receiver can route on the header without parsing the body.X-Piprio-Signaturecarries the HMAC signature described below.
Signature verification
Each webhook has its own signing secret. The secret is a 64-character hexadecimal string (32 random bytes) generated when the webhook is created. It is returned exactly once, in the response to the create request, and is never shown again. Piprio stores it encrypted at rest and decrypts it only at delivery time. The secret should be saved by the receiving system at registration.
Piprio signs every delivery with HMAC using SHA-256. The signed message is the exact bytes of the JSON request body. The resulting hex digest is sent in the X-Piprio-Signature header, prefixed with the algorithm name:
X-Piprio-Signature: sha256=3f8a... (64 hex characters)
To verify a delivery, the receiver computes HMAC-SHA256 over the raw request body using the stored secret, then compares the result against the value after the sha256= prefix. The comparison must be done against the raw bytes of the body as received, before any deserialization. Parsing the JSON and re-serializing it would reorder or reformat keys and produce a different signature, so the raw body must be captured for verification.
A verification routine in Python looks like this:
import hmac
import hashlib
def verify(request_body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(), request_body, hashlib.sha256
).hexdigest()
sent = signature_header.removeprefix("sha256=")
return hmac.compare_digest(expected, sent)
request_body is the raw, undecoded body of the incoming request. signature_header is the value of the X-Piprio-Signature header. secret is the signing secret saved at registration. Using a constant-time comparison such as hmac.compare_digest avoids leaking information through timing. A request whose signature does not match should be rejected.
Retry policy
A delivery has a 10-second timeout. A delivery is treated as successful when the receiver responds with a 2xx status code. Any other status code, a timeout, or a connection error counts as a failure.
When a delivery fails, Piprio retries it with exponential backoff. Each dispatched delivery makes one initial attempt followed by up to three retries, for a maximum of four attempts. The retries are spaced 30 seconds, then 120 seconds, then 300 seconds after the preceding failure. After the fourth attempt the delivery for that event is abandoned.
Separately from per-event retries, Piprio tracks a running count of consecutive failures for each webhook. The count resets to zero on any successful delivery. When the count reaches 10 consecutive failures, the webhook is deactivated automatically: no further events are delivered to it, and the organization's owners receive a notification telling them to check the endpoint and re-activate the webhook in Settings > Integrations. Re-activating a webhook resets its failure count to zero. These two mechanisms are independent: the four-attempt limit governs a single event delivery, while the count of 10 governs whether the webhook stays active over time.
Delivery is idempotent. Once an event has been delivered to a webhook, that exact delivery is recorded for 24 hours, and a later retry of the same event to the same webhook is skipped rather than sent twice. A receiver that responds slowly and then sees a retry should still de-duplicate on its side, but Piprio will not knowingly send the same event to the same endpoint more than once within that window.
Testing webhooks locally
Piprio includes a built-in test-delivery endpoint. Sending a request to the webhook's test route (POST /webhooks/{webhook_id}/test) dispatches a single delivery to the registered URL with the test event identifier and a fixed body:
{
"event": "test",
"timestamp": "2026-05-21T14:32:10.123456+00:00",
"org_slug": "acme",
"data": {
"message": "This is a test webhook delivery from Piprio."
}
}
The test delivery is signed and carries the same headers as a real event, so it exercises the full signature-verification path on the receiving end. The endpoint responds as soon as the delivery is queued. A success response confirms that the test was dispatched, not that the receiver accepted it. The actual outcome shows up in the webhook's last-triggered timestamp and last status code, which are visible in Settings > Integrations.
To test against a local receiver during development, a developer can point a webhook at a tunneling service (such as a local forwarding proxy) that exposes a development server on a public HTTPS URL, register that URL as the webhook endpoint, and trigger the test delivery. The signing secret returned at registration is used to verify the signature on incoming requests exactly as a production receiver would.