Every webhook delivery from Scope includes a cryptographic signature so you can verify it originated from us and hasn't been tampered with.
Each webhook POST request includes two security headers:
| Header | Description |
|---|---|
x-scope-signature |
HMAC-SHA256 hex digest of timestamp.body using your webhook secret. |
x-scope-timestamp |
Unix timestamp (seconds) when the payload was signed. |
https://.x-scope-timestamp and x-scope-signature from the request headers.{timestamp}.{raw_body}whsec_...) and the signed content.x-scope-signature header using a timing-safe comparison.const crypto = require('crypto');
function verifyWebhook(req, secret) {
const signature = req.headers['x-scope-signature'];
const timestamp = req.headers['x-scope-timestamp'];
if (!signature || !timestamp) return false;
// Reject requests older than 5 minutes
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (Math.abs(age) > 300) return false;
const signedContent = `${timestamp}.${req.rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedContent)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express example
app.post('/webhooks/scope', express.raw({ type: 'application/json' }), (req, res) => {
req.rawBody = req.body.toString();
if (!verifyWebhook(req, process.env.SCOPE_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.rawBody);
console.log('Verified event:', event.event, event.data);
res.status(200).send('ok');
});
import hmac, hashlib, time, json
def verify_webhook(headers: dict, body: bytes, secret: str) -> bool:
signature = headers.get('x-scope-signature', '')
timestamp = headers.get('x-scope-timestamp', '')
if not signature or not timestamp:
return False
# Reject requests older than 5 minutes
age = abs(int(time.time()) - int(timestamp))
if age > 300:
return False
signed_content = f"{timestamp}.{body.decode('utf-8')}"
expected = hmac.new(
secret.encode('utf-8'),
signed_content.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhooks/scope', methods=['POST'])
def handle_webhook():
if not verify_webhook(request.headers, request.data, SCOPE_WEBHOOK_SECRET):
abort(401)
event = json.loads(request.data)
print(f"Verified event: {event['event']}", event['data'])
return 'ok', 200
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"math"
"net/http"
"strconv"
"time"
)
func verifyWebhook(r *http.Request, secret string) ([]byte, bool) {
sig := r.Header.Get("X-Scope-Signature")
ts := r.Header.Get("X-Scope-Timestamp")
if sig == "" || ts == "" {
return nil, false
}
tsInt, err := strconv.ParseInt(ts, 10, 64)
if err != nil || math.Abs(float64(time.Now().Unix()-tsInt)) > 300 {
return nil, false
}
body, _ := io.ReadAll(r.Body)
signedContent := fmt.Sprintf("%s.%s", ts, string(body))
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signedContent))
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(sig), []byte(expected)) {
return nil, false
}
return body, true
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, ok := verifyWebhook(r, "whsec_your_secret_here")
if !ok {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
fmt.Printf("Verified: %s\n", string(body))
w.WriteHeader(http.StatusOK)
}
All webhook payloads follow this envelope structure (timestamp is a Unix seconds string):
{
"event": "candidate.scored",
"timestamp": "1739323200",
"organization_id": "org_uuid",
"data": {
"candidate_id": "cand_uuid",
"project_id": "proj_uuid",
"candidate_score": 8.2,
"recommendation": "Interested",
...
}
}
| Event | Trigger |
|---|---|
candidate.created | A new candidate is added to any project. |
candidate.scored | AI analysis completes for a candidate. |
candidate.updated | Candidate data changes (status, enrichment, etc.). |
feedback.created | A team member gives feedback on a candidate. |
Important: Always verify signatures before processing webhook payloads. Never trust the payload contents without verification. Store your webhook secret securely - treat it like a password.
2xx response within 10 seconds.Use a tunneling service to test webhooks against localhost:
# Using ngrok
ngrok http 3000
# Then register the ngrok URL in Scope Developer Settings:
# https://abc123.ngrok.io/webhooks/scope