Webhooks
Receive real-time HTTP notifications when things happen to pull requests in your team's repositories.
pr.ready_to_merge, review.round_complete, review.sla_breached, and pr.attention_needed.Setting up a webhook
Via the dashboard
- Navigate to Settings → Webhooks in your Tenpace dashboard.
- Click Add Endpoint.
- Enter your HTTPS endpoint URL.
- Select the events you want to subscribe to (or choose All events).
- Click Create — you'll see the webhook secret once. Copy it now.
Via the API
POST /api/webhooks
Authorization: Bearer <api-key>
Content-Type: application/json
{
"url": "https://example.com/hooks/tenpace",
"events": ["pr.ready_to_merge", "review.sla_breached"],
"description": "Merge automation handler"
} The response includes a secret field — this is the only time the raw secret is returned. Store it securely.
Verifying signatures
Every webhook request includes an X-Tenpace-Signature header. Always verify it before processing the payload to prevent spoofed requests.
The signature is computed as sha256=HMAC-SHA256(secret, body).
Node.js
const crypto = require('crypto');
function verifySignature(secret, rawBody, signatureHeader) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
);
}
// Express example
app.post('/hooks/tenpace', express.raw({ type: '*/*' }), (req, res) => {
const sig = req.headers['x-tenpace-signature'];
if (!verifySignature(process.env.WEBHOOK_SECRET, req.body, sig)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log('Received event:', event.event);
res.sendStatus(200);
}); Python
import hmac, hashlib
def verify_signature(secret: str, body: bytes, header: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)
# Flask example
@app.route('/hooks/tenpace', methods=['POST'])
def webhook():
sig = request.headers.get('X-Tenpace-Signature', '')
if not verify_signature(os.environ['WEBHOOK_SECRET'], request.get_data(), sig):
return 'Invalid signature', 401
event = request.get_json(force=True)
print(f"Received event: {event['event']}")
return '', 200 Go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func verifySignature(secret string, body []byte, header string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(header))
} Request headers
| Header | Description |
|---|---|
X-Tenpace-Signature | HMAC-SHA256 of the request body, prefixed with sha256= |
X-Tenpace-Event | Event type string, e.g. pr.opened |
X-Tenpace-Delivery | Unique delivery ID (UUID-like) for idempotency |
X-Tenpace-Timestamp | Unix timestamp (seconds) of the delivery |
Content-Type | application/json |
Payload envelope
All events share the same top-level structure:
{
"event": "pr.opened", // Event type
"timestamp": "2025-01-01T12:00:00.000Z",
"data": { ... } // Event-specific payload
} Retry policy
If your endpoint returns a non-2xx status or times out, Tenpace retries delivery with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 10 seconds |
| 2nd retry | 60 seconds |
| 3rd retry | 5 minutes |
After 3 retries the delivery is marked failed. If an endpoint accumulates 10 consecutive failures across any deliveries, it is automatically disabled to prevent abuse. You can re-enable it from the dashboard or API.
Each delivery attempt times out after 10 seconds. Make sure your endpoint responds quickly — if you need to do heavy processing, respond 200 immediately and process asynchronously.
Event types
See the full Event Reference → for complete payloads and examples.
GitHub Pass-through
pr.openedpr.mergedpr.closedpr.review_submittedpr.review_requestedpr.ci_status
Tenpace Workflow Events
pr.ready_to_mergepr.attention_neededreview.round_completereview.sla_breached