Receiving & Verifying Webhooks
Your endpoint
Astalty sends an HTTP POST request with a JSON body when a subscribed event occurs. Your endpoint should:
- Accept
POSTrequests with a JSON body. - Verify the signature before trusting the request.
- Return a
2xxstatus code, such as200 OK, within 15 seconds.
Queue longer work and acknowledge the delivery first. A timeout is treated as a failed attempt.
The payload
Every webhook delivery has the same top-level shape:
{
"id": "9ee9dc30-c869-4fb5-9871-b32850e68cad",
"event_type": "participant.created",
"resource": {
"id": "9ee9dc24-36e4-4e02-b734-32d982b83089",
"type": "participant"
},
"data": []
}
| Field | Description |
|---|---|
id | The unique ID for this delivery. Retries of the same delivery keep this ID. |
event_type | The event type, such as task.created. |
resource.id | The Astalty UUID of the related record. |
resource.type | The related record type, such as participant. |
data | Event-specific data. Most events send an empty array. job_application.created sends an object with screening_outcome. |
Webhook payloads identify what happened and which record it concerns. They do not embed the full record. If the public API offers a matching endpoint, use resource.id to retrieve the record's current state. For example, use GET /public/v1/participants/{participant} for a Participant event.
For job_application.created, data.screening_outcome is meets_all, missing_must_have, missing_nice_to_have or null. This is the screening result when the application is created.
Verifying the signature
Every delivery includes this signature header:
X-Astalty-Signature: <signature>
The value is a lowercase hexadecimal HMAC-SHA256 digest of the exact JSON request body, using the webhook's Signing Secret as the key. Read the raw body, calculate the digest and compare it with the header using a constant-time comparison.
// PHP example
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_ASTALTY_SIGNATURE'] ?? '';
$expected = hash_hmac('sha256', $payload, $signingSecret);
if (! hash_equals($expected, $signature)) {
http_response_code(403);
exit;
}
// The signature is valid. The event can now be processed.
// Node.js (Express) example
const crypto = require("crypto");
function verify(rawBody, signatureHeader, signingSecret) {
const expected = crypto
.createHmac("sha256", signingSecret)
.update(rawBody)
.digest("hex");
const expectedBytes = Buffer.from(expected);
const receivedBytes = Buffer.from(signatureHeader || "");
return (
expectedBytes.length === receivedBytes.length &&
crypto.timingSafeEqual(expectedBytes, receivedBytes)
);
}
Retries and delivery
A delivery succeeds when the endpoint returns any 2xx status within 15 seconds. Astalty retries when the endpoint cannot be reached, times out or returns a non-2xx status.
Astalty attempts each delivery up to 10 times:
- The initial attempt and 2 quick retries, each with a 5-second delay.
- 7 scheduled attempts, with delays of 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 24 hours.
Queue processing can make an attempt run later than its scheduled delay. After all attempts fail, the delivery is marked Failed. From the webhook detail page, you can manually retry a Failed delivery or resend a Delivered one. See Review Webhook Delivery Logs.
Design the endpoint to process the same delivery id more than once without creating duplicate changes.