Webhooks
Receive notifications when test runs complete
Webhooks
Webhooks notify your server when test runs complete, eliminating the need for polling.
Setup
- Go to Dashboard → Projects
- Edit a project
- Enter your Webhook URL
- A Webhook Secret is automatically generated
Each project has its own webhook configuration. Test plans linked to that project will trigger webhooks when complete.
Webhook Payload
When a test completes, Test-Lab sends a POST request to your webhook URL:
{
"event": "run.completed",
"jobId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"testPlanId": YOUR_TEST_PLAN_ID,
"buildId": "YOUR_BUILD_ID",
"status": "completed",
"result": {
"summary": {
"result": "passed",
"notes": "All acceptance criteria verified."
},
"acceptanceCriteria": [
{
"description": "Homepage loads",
"status": "passed",
"evidence": ["Page loaded in 1.2s"]
}
],
"steps": [...],
"issues": []
},
"error": null,
"completedAt": "2024-01-15T10:32:05.000Z"
}Event Types
| Event | Description |
|---|---|
run.completed | Test finished successfully (may have passed or failed) |
run.failed | Test execution error (not a test failure) |
Headers
| Header | Description |
|---|---|
Content-Type | application/json |
X-TestLab-Signature | HMAC-SHA256 signature |
X-TestLab-Event | Event type |
User-Agent | TestLab-Webhook/1.0 |
Verifying Signatures
Always verify webhook signatures to ensure requests are from Test-Lab.
The X-TestLab-Signature header contains:
sha256=<hex-encoded-hmac>Node.js Example
import crypto from 'crypto';
function verifySignature(payload, signature, secret) {
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express.js handler
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-testlab-signature'];
const payload = req.body.toString();
if (!verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
console.log(`Test ${event.jobId}: ${event.result?.summary?.result}`);
res.status(200).send('OK');
});Python Example
import hmac
import hashlib
from flask import Flask, request
app = Flask(__name__)
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-TestLab-Signature')
payload = request.get_data()
if not verify_signature(payload, signature, os.environ['WEBHOOK_SECRET']):
return 'Invalid signature', 401
event = request.json
print(f"Test {event['jobId']}: {event['result']['summary']['result']}")
return 'OK', 200Go Example
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
)
func verifySignature(payload []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
payload, _ := io.ReadAll(r.Body)
signature := r.Header.Get("X-TestLab-Signature")
if !verifySignature(payload, signature, os.Getenv("WEBHOOK_SECRET")) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process event...
w.WriteHeader(http.StatusOK)
}Payload Fields
| Field | Type | Description |
|---|---|---|
event | string | run.completed or run.failed |
jobId | string | Job UUID |
testPlanId | number | Test plan ID |
buildId | string | null | CI build ID if provided |
status | string | completed or failed |
result | object | null | Test results |
error | string | null | Error message if execution failed |
completedAt | string | ISO timestamp |
Best Practices
Respond Quickly
Return a 200 status within 30 seconds. Process events asynchronously if needed:
app.post('/webhook', (req, res) => {
// Respond immediately
res.status(200).send('OK');
// Process asynchronously
processWebhook(req.body).catch(console.error);
});Handle Retries
Webhooks are delivered once. If your server is down, you'll miss the event.
Store the Secret Securely
Never commit your webhook secret to version control. Use environment variables:
WEBHOOK_SECRET=your-secret-hereTesting Webhooks
Local Development
Use tools like ngrok to expose your local server:
ngrok http 3000
# Use the ngrok URL as your webhook URLVerify Configuration
After setting up, trigger a test and check your webhook endpoint receives the payload.
Troubleshooting
Not Receiving Webhooks
- Verify the webhook URL is correct and publicly accessible
- Check your server logs for incoming requests
- Ensure your server returns
200status
Signature Verification Failing
- Use the raw request body (before JSON parsing)
- Ensure you're using the correct webhook secret
- Check for encoding issues