Documentation Index
Fetch the complete documentation index at: https://orbit-docs.devotel.io/llms.txt
Use this file to discover all available pages before exploring further.
Webhook Security
Every webhook delivery from Orbit includes an HMAC-SHA256 signature in the X-Devotel-Signature header. Always verify this signature to ensure the request genuinely came from Orbit and hasn’t been tampered with.
The X-Devotel-Signature header is Stripe-style, not a bare hex string:
X-Devotel-Signature: t=1715357600,v1=4f9c2e6b8a1d3f5e7c9b1a3d5f7e9c1b3a5d7f9e1c3b5a7d9f1e3c5b7a9d1f3e
t=<unix> — Unix timestamp (seconds) when Orbit signed the payload. Use this for replay protection.
v1=<hex> — HMAC-SHA256 of the string <t>.<raw_body> keyed by your webhook signing secret. Hex-encoded.
During a secret rotation grace window, Orbit emits two v1=<hex> values in the same header (new secret first, previous secret second) so a verifier configured with either secret continues to validate:
X-Devotel-Signature: t=1715357600,v1=<sig_with_new_secret>,v1=<sig_with_prev_secret>
Your verifier should split the header by ,, parse each key=value pair, and accept the request if any v1 matches your computed HMAC.
How Signature Verification Works
- Parse the
X-Devotel-Signature header into a timestamp t and one or more v1 candidate signatures.
- Reject the request if
t is older than 5 minutes (replay protection).
- Compute
expected = HMAC_SHA256(secret, "<t>.<raw_body>") as hex.
- Compare
expected against each v1 candidate using a timing-safe comparison.
Verification Examples
Node.js
import crypto from 'node:crypto';
function parseSignatureHeader(header) {
const parts = header.split(',').map((p) => p.trim());
let t = null;
const v1s = [];
for (const part of parts) {
const eq = part.indexOf('=');
if (eq < 0) continue;
const key = part.slice(0, eq);
const value = part.slice(eq + 1);
if (key === 't') t = value;
else if (key === 'v1') v1s.push(value);
}
return { t, v1s };
}
function verifyWebhookSignature(rawBody, header, secret) {
const { t, v1s } = parseSignatureHeader(header);
if (!t || v1s.length === 0) return false;
// Replay protection: reject signatures older than 5 minutes.
const ageSec = Math.floor(Date.now() / 1000) - Number(t);
if (!Number.isFinite(ageSec) || ageSec < 0 || ageSec > 5 * 60) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`, 'utf-8')
.digest('hex');
const expectedBuf = Buffer.from(expected, 'hex');
// Accept the request if ANY v1 matches (covers rotation grace).
return v1s.some((candidate) => {
const candidateBuf = Buffer.from(candidate, 'hex');
return (
candidateBuf.length === expectedBuf.length &&
crypto.timingSafeEqual(candidateBuf, expectedBuf)
);
});
}
// In your webhook handler:
app.post('/webhooks/orbit', (req, res) => {
const header = req.headers['x-devotel-signature'];
const isValid = verifyWebhookSignature(req.rawBody, header, 'whsec_your_secret');
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(req.rawBody);
// Process the event...
res.status(200).json({ received: true });
});
Python
import hmac
import hashlib
import time
def parse_signature_header(header: str):
t = None
v1s = []
for part in header.split(","):
part = part.strip()
if "=" not in part:
continue
key, _, value = part.partition("=")
if key == "t":
t = value
elif key == "v1":
v1s.append(value)
return t, v1s
def verify_webhook_signature(raw_body: bytes, header: str, secret: str) -> bool:
t, v1s = parse_signature_header(header)
if not t or not v1s:
return False
# Replay protection: reject signatures older than 5 minutes.
age = int(time.time()) - int(t)
if age < 0 or age > 5 * 60:
return False
signed = f"{t}.".encode("utf-8") + raw_body
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
# Accept the request if ANY v1 matches (covers rotation grace).
return any(hmac.compare_digest(candidate, expected) for candidate in v1s)
# In your webhook handler (Flask example):
@app.route("/webhooks/orbit", methods=["POST"])
def handle_webhook():
header = request.headers.get("X-Devotel-Signature", "")
if not verify_webhook_signature(request.data, header, "whsec_your_secret"):
return jsonify({"error": "Invalid signature"}), 401
event = request.get_json()
# Process the event...
return jsonify({"received": True}), 200
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
"strings"
"time"
)
func parseSignatureHeader(header string) (string, []string) {
var t string
var v1s []string
for _, part := range strings.Split(header, ",") {
part = strings.TrimSpace(part)
eq := strings.IndexByte(part, '=')
if eq < 0 {
continue
}
key, value := part[:eq], part[eq+1:]
switch key {
case "t":
t = value
case "v1":
v1s = append(v1s, value)
}
}
return t, v1s
}
func verifyWebhookSignature(rawBody []byte, header, secret string) bool {
t, v1s := parseSignatureHeader(header)
if t == "" || len(v1s) == 0 {
return false
}
// Replay protection: reject signatures older than 5 minutes.
ts, err := strconv.ParseInt(t, 10, 64)
if err != nil {
return false
}
age := time.Now().Unix() - ts
if age < 0 || age > 5*60 {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(t + "."))
mac.Write(rawBody)
expected := hex.EncodeToString(mac.Sum(nil))
// Accept the request if ANY v1 matches (covers rotation grace).
for _, candidate := range v1s {
if hmac.Equal([]byte(candidate), []byte(expected)) {
return true
}
}
return false
}
Security Best Practices
- Always verify signatures — never process webhooks without checking the signature
- Use timing-safe comparison — prevents timing attacks (
crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python)
- Use HTTPS — your webhook endpoint must use HTTPS to protect payloads in transit
- Rotate secrets — rotate your webhook signing secret periodically in Settings > Webhooks
- Idempotency — use the event
id field to deduplicate, since Orbit guarantees at-least-once delivery
- Respond quickly — return a
2xx within 30 seconds; process heavy work asynchronously
- IP allowlisting — optionally restrict incoming webhooks to Orbit’s IP ranges (available in the dashboard under Settings > Security)
Signing Secret
Your webhook signing secret is generated when you create a webhook. Retrieve it from the dashboard under Webhooks > [Your Webhook] > Signing Secret, or via the API:
curl https://orbit-api.devotel.io/api/v1/webhooks/wh_abc123 \
-H "X-API-Key: dv_live_sk_..."
The secret is prefixed with whsec_ and should be stored securely — never expose it in client-side code or logs.