Webhooks
Webhooks deliver real-time event notifications to your application via HTTP POST requests. Instead of polling the API, your server receives updates the moment something happens.
How webhooks work
Supported events
| Event | Description | Fired when |
|---|---|---|
tracking.updated | Tracking status changed | Any status transition (accepted, in_transit, out_for_delivery) |
tracking.delivered | Package delivered | Final delivery confirmation |
tracking.exception | Delivery exception occurred | Failed attempt, hold, damage, or customs issue |
label.created | New label purchased | POST /v1/labels succeeds |
label.cancelled | Label voided | POST /v1/labels/:id/cancel succeeds |
shipment.rate_available | Rate quote completed | Async rate request finishes |
Registering a webhook
curl -X POST https://api.flexops.io/v1/webhooks \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/flexops",
"events": ["tracking.updated", "tracking.delivered"],
"secret": "whsec_your_signing_secret"
}'
Use a unique secret per webhook endpoint. This secret is used to generate HMAC signatures so your server can verify that payloads are authentic.
Webhook payload
{
"id": "evt_abc123",
"type": "tracking.updated",
"createdAt": "2026-03-10T14:30:00Z",
"data": {
"trackingNumber": "9400111899223456789012",
"carrier": "usps",
"status": "in_transit",
"previousStatus": "accepted",
"estimatedDelivery": "2026-03-12T17:00:00Z"
}
}
Verifying webhook signatures
Every webhook includes an X-FlexOps-Signature header containing an HMAC-SHA256 signature. Always verify this signature before processing the payload.
- Node.js
- Python
- C#
- Ruby
- PHP
- Go
- Java
import crypto from 'crypto';
function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your Express handler:
app.post('/webhooks/flexops', (req, res) => {
const isValid = verifyWebhook(
req.body,
req.headers['x-flexops-signature'],
process.env.FLEXOPS_WEBHOOK_SECRET
);
if (!isValid) return res.status(401).send('Invalid signature');
const event = JSON.parse(req.body);
// Process event...
res.status(200).send('OK');
});
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
using System.Security.Cryptography;
using System.Text;
// Or use the SDK helper:
// bool isValid = FlexOpsClient.VerifyWebhook(payload, signature, secret);
public static bool VerifyWebhook(string payload, string signature, string secret)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = Convert.ToHexString(
hmac.ComputeHash(Encoding.UTF8.GetBytes(payload))
).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(signature),
Encoding.UTF8.GetBytes(expected)
);
}
require 'openssl'
def verify_webhook(payload, signature, secret)
expected = OpenSSL::HMAC.hexdigest('sha256', secret, payload)
Rack::Utils.secure_compare(signature, expected)
end
function verifyWebhook(string $payload, string $signature, string $secret): bool
{
$expected = hash_hmac('sha256', $payload, $secret);
return hash_equals($expected, $signature);
}
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func VerifyWebhook(payload []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
// Or use the SDK helper:
// boolean isValid = FlexOpsClient.verifyWebhook(payload, signature, secret);
public static boolean verifyWebhook(String payload, String signature, String secret) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
String expected = bytesToHex(mac.doFinal(payload.getBytes()));
return MessageDigest.isEqual(signature.getBytes(), expected.getBytes());
}
Retry policy
Failed webhook deliveries (non-2xx response or timeout) are retried with exponential backoff:
| Attempt | Delay | Cumulative time |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 1 minute | 1 min |
| 3 | 5 minutes | 6 min |
| 4 | 30 minutes | 36 min |
| 5 | 2 hours | 2 hr 36 min |
| 6 | 12 hours | 14 hr 36 min |
After 6 failed attempts, the webhook endpoint is disabled. Re-enable it from the Dashboard or via POST /v1/webhooks/:id/enable.
Your webhook endpoint must respond within 30 seconds. Process events asynchronously — acknowledge the webhook immediately with 200 OK, then handle the business logic in a background job.
Best practices
- Always verify signatures — Never trust webhook payloads without HMAC verification
- Respond quickly — Return
200 OKbefore processing; use a queue for heavy work - Handle duplicates — Use the
idfield to deduplicate; the same event may be delivered more than once - Monitor delivery — Check webhook delivery status in the Dashboard under Settings > Webhooks > Delivery Log
- Use HTTPS — Webhook URLs must use HTTPS in production (HTTP allowed in sandbox)