Test-Lab.aiDocs

Webhooks

Receive notifications when test runs complete

Webhooks

Webhooks notify your server when test runs complete, eliminating the need for polling.

Setup

  1. Go to DashboardProjects
  2. Edit a project
  3. Enter your Webhook URL
  4. 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

EventDescription
run.completedTest finished successfully (may have passed or failed)
run.failedTest execution error (not a test failure)

Headers

HeaderDescription
Content-Typeapplication/json
X-TestLab-SignatureHMAC-SHA256 signature
X-TestLab-EventEvent type
User-AgentTestLab-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', 200

Go 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

FieldTypeDescription
eventstringrun.completed or run.failed
jobIdstringJob UUID
testPlanIdnumberTest plan ID
buildIdstring | nullCI build ID if provided
statusstringcompleted or failed
resultobject | nullTest results
errorstring | nullError message if execution failed
completedAtstringISO 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-here

Testing Webhooks

Local Development

Use tools like ngrok to expose your local server:

ngrok http 3000
# Use the ngrok URL as your webhook URL

Verify Configuration

After setting up, trigger a test and check your webhook endpoint receives the payload.

Troubleshooting

Not Receiving Webhooks

  1. Verify the webhook URL is correct and publicly accessible
  2. Check your server logs for incoming requests
  3. Ensure your server returns 200 status

Signature Verification Failing

  1. Use the raw request body (before JSON parsing)
  2. Ensure you're using the correct webhook secret
  3. Check for encoding issues

Next Steps

On this page