API Documentation
All APIs use Bearer token authentication. Get your API key
Authentication
Every request must include your API key in the Authorization header as a Bearer token. Keys are scoped to a single product — using a key against the wrong product returns 403.
Key format
Keys follow the pattern dops_live_<prefix>_<32hex>. The prefix tells you which product the key grants access to.
| Prefix | Product | Example key (truncated) |
|---|---|---|
| weh | Webhook Reliability | dops_live_weh_a1b2c3d4... |
| sec | Secrets Leak Detection | dops_live_sec_e5f6a7b8... |
| pii | PII Scrubbing | dops_live_pii_c9d0e1f2... |
| dep | Deployment Risk Score | dops_live_dep_a3b4c5d6... |
| rat | Distributed Rate Limiting | dops_live_rat_e7f8a9b0... |
| api | API Health Monitor | dops_live_api_c1d2e3f4... |
How to get a key
Visit /dashboard/api-keys and click New API Key. Choose the product, give it a label, and copy the key immediately — it is only shown once.
Test your key
curl https://devopsil.com/api/v1/webhooks/endpoints \
-H "Authorization: Bearer dops_live_weh_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
# 200 OK — key is valid
# 401 — key missing, malformed, or revoked
# 403 — key is for a different productHow to rotate keys
Create a new key in the dashboard before revoking the old one so you have zero-downtime rotation:
- Go to /dashboard/api-keys and create a new key for the same product.
- Deploy your application with the new key in your environment / secrets manager.
- Verify traffic is flowing successfully with the new key.
- Return to the dashboard and revoke the old key.
Errors
All errors return JSON with an error string. Some errors include additional fields.
| Status | Meaning |
|---|---|
| 401 | Missing or invalid Authorization header / key revoked |
| 403 | Key exists but is not authorized for this product |
| 400 | Invalid request body or parameters |
| 404 | Resource not found or not owned by your account |
| 422 | Request is valid JSON but cannot be processed (e.g. unsupported URL scheme) |
| 429 | Per-minute rate limit or monthly quota exceeded |
| 500 | Internal server error — safe to retry with backoff |
Error response bodies
# 401 — missing or malformed header
{ "error": "Missing or invalid Authorization header. Use: Authorization: Bearer dops_live_..." }
# 401 — key not found / revoked
{ "error": "API key not found" }
# 403 — wrong product
{ "error": "This API key is not authorized for the 'secrets-leak' product" }
# 429 — per-minute burst
{
"error": "Rate limit exceeded",
"retry_after": 60,
"remaining": 0
}
# 429 — monthly quota
{
"error": "Monthly quota exhausted",
"quota_limit": 500,
"quota_used": 500,
"reset_at": "2026-05-01T00:00:00Z"
}
# 400 — invalid body
{ "error": "Field 'url' must be a valid HTTPS URL" }
# 404 — not found
{ "error": "Endpoint not found" }Rate Limits & Response Headers
Every response includes rate-limit headers. Two independent limits apply: a per-minute burst window and a monthly quota. Both are enforced independently — you can exhaust your quota without hitting the burst limit, and vice versa.
| Header | Description |
|---|---|
| X-RateLimit-Limit | Per-minute burst limit for your billing tier |
| X-RateLimit-Remaining | Remaining burst calls in the current 60-second window |
| X-RateLimit-Reset | Unix timestamp when the burst window resets |
| X-Quota-Used | Total API calls consumed in the current billing period |
| X-Quota-Limit | Total API calls allowed in the current billing period |
| Retry-After | Seconds to wait before retrying (only present on 429 responses) |
| Tier | Monthly | Per minute |
|---|---|---|
| Free | 500 | 10 |
| Developer | 10,000 | 60 |
| Starter | 50,000 | 120 |
| Team | 200,000 | 300 |
| Business | 1,000,000 | 600 |
| Enterprise | Unlimited | 3,000 |
Handling 429 with exponential backoff
// TypeScript — exponential backoff helper
async function fetchWithRetry(
url: string,
options: RequestInit,
maxRetries = 5
): Promise<Response> {
let attempt = 0;
while (true) {
const res = await fetch(url, options);
if (res.status !== 429 || attempt >= maxRetries) return res;
const retryAfter = parseInt(res.headers.get("Retry-After") ?? "1", 10);
const jitter = Math.random() * 500; // up to 500ms random jitter
const delay = retryAfter * 1000 * Math.pow(2, attempt) + jitter;
console.warn(`Rate limited. Retrying in ${Math.round(delay)}ms (attempt ${attempt + 1})`);
await new Promise((r) => setTimeout(r, delay));
attempt++;
}
}
// Usage
const res = await fetchWithRetry(
"https://devopsil.com/api/v1/webhooks/endpoints",
{ headers: { Authorization: "Bearer dops_live_weh_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6" } }
);Webhook Reliability API
Base URL: https://devopsil.com/api/v1/webhooks · Key prefix: weh
Register destination endpoints, ingest events, and the platform handles reliable delivery with automatic retries, HMAC-SHA256 signature verification, idempotency deduplication, and per-event delivery history.
Quick Start
# 1. Register a destination endpoint
curl -X POST https://devopsil.com/api/v1/webhooks/endpoints \
-H "Authorization: Bearer dops_live_weh_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6" \
-H "Content-Type: application/json" \
-d '{"url":"https://your-server.com/hooks","events":["order.*"],"description":"Order events"}'
# 2. Ingest an event (triggers delivery to all matching endpoints)
curl -X POST https://devopsil.com/api/v1/webhooks/events \
-H "Authorization: Bearer dops_live_weh_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6" \
-H "Content-Type: application/json" \
-d '{"type":"order.created","data":{"order_id":"ord_123","amount":4900},"idempotency_key":"ord_123_created"}'
# 3. Check delivery status
curl https://devopsil.com/api/v1/webhooks/events/42 \
-H "Authorization: Bearer dops_live_weh_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"Event Pattern Matching
The events array on a registered endpoint controls which events are delivered. Three matching modes are supported:
| Pattern | Mode | Matches |
|---|---|---|
| * | Wildcard | Every event regardless of type |
| order.* | Prefix wildcard | order.created, order.failed, order.refunded, etc. |
| order.created | Exact | Only the exact type order.created |
Retry Schedule
If a destination returns a non-2xx status code or times out (>30s), delivery is retried on this schedule. A delivery is considered permanently failed after attempt 8.
| Attempt | Delay after previous attempt | Total elapsed |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 1 second | ~1s |
| 3 | 5 seconds | ~6s |
| 4 | 30 seconds | ~36s |
| 5 | 5 minutes | ~5m 36s |
| 6 | 30 minutes | ~35m 36s |
| 7 | 2 hours | ~2h 35m |
| 8 | 24 hours | ~26h 35m |
Delivered Payload Format
When an event is delivered to your endpoint, the platform sends an HTTP POST with these headers and body:
POST https://your-server.com/hooks
Content-Type: application/json
X-Webhook-ID: 42
X-Webhook-Type: order.created
X-Webhook-Timestamp: 1712345678
X-Webhook-Signature: sha256=3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b
{
"id": 42,
"type": "order.created",
"data": {
"order_id": "ord_123",
"amount": 4900
},
"timestamp": 1712345678
}Idempotency Keys
Pass an idempotency_key when ingesting events to prevent duplicate deliveries caused by network retries or application restarts. The platform deduplicates within a 24-hour window — a second ingest with the same key returns the original event ID with HTTP 200 and no re-delivery.
Recommended format: <entity_id>_<action> — e.g. ord_123_created or a UUID generated once and stored alongside the entity in your database.
Endpoints
Node.js Webhook Receiver (signature verification)
// Express webhook receiver with HMAC-SHA256 signature verification
import crypto from "crypto";
import express from "express";
const app = express();
// IMPORTANT: use raw body parser so you can compute the HMAC over the exact bytes sent
app.post("/hooks", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-webhook-signature"] as string;
const secret = process.env.WEBHOOK_SECRET!; // whsec_9f8e7d6c...
if (!signature?.startsWith("sha256=")) {
return res.status(401).json({ error: "Missing signature" });
}
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(req.body) // raw Buffer
.digest("hex");
const isValid = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
if (!isValid) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(req.body.toString());
console.log("Received event:", event.type, event.id);
// Acknowledge immediately; process asynchronously
res.status(200).json({ received: true });
// Handle event types
if (event.type === "order.created") {
// ... process order
}
});
app.listen(3000);Python Webhook Receiver (Flask)
import hmac
import hashlib
import os
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["WEBHOOK_SECRET"] # whsec_9f8e7d6c...
@app.route("/hooks", methods=["POST"])
def webhook():
signature = request.headers.get("X-Webhook-Signature", "")
if not signature.startswith("sha256="):
abort(401)
raw_body = request.get_data()
expected = "sha256=" + hmac.new(
WEBHOOK_SECRET.encode(),
raw_body,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401)
event = request.get_json()
print(f"Received {event['type']} (id={event['id']})")
# Acknowledge immediately
return jsonify(received=True), 200
if __name__ == "__main__":
app.run(port=3000)Real-World Use Cases
E-commerce order pipeline
Your checkout service fires a single order.created event. Three downstream services — warehouse, email, and analytics — each receive it independently with full retry guarantees. If your email service is down for 2 hours, it catches up automatically when it recovers.
// On checkout complete — one ingest call fans out to all subscribers
await fetch("https://devopsil.com/api/v1/webhooks/events", {
method: "POST",
headers: { Authorization: "Bearer " + process.env.DEVOPSIL_WEH_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
type: "order.created",
data: { order_id: "ord_9kX2", customer_id: 42, total_cents: 9900, items: [...] },
idempotency_key: "ord_9kX2_created", // safe to retry on network error
}),
});
// Registered endpoints (each team owns their own):
// warehouse-api.internal/hooks → listens for order.*
// email-service.internal/hooks → listens for order.*
// analytics-pipeline.internal/ → listens for *GitHub → Slack + deploy pipeline
A GitHub Actions workflow ingests build results as structured events. Your Slack bot and deployment dashboard both receive them via registered endpoints — no custom fanout logic needed.
# .github/workflows/notify.yml
- name: Notify on build complete
if: always()
run: |
curl -s -X POST https://devopsil.com/api/v1/webhooks/events \
-H "Authorization: Bearer $DEVOPSIL_WEH_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg status "${{ job.status }}" \
--arg repo "${{ github.repository }}" \
--arg sha "${{ github.sha }}" \
--arg run "${{ github.run_id }}" \
'{type: "build.completed", data: {status: $status, repo: $repo, sha: $sha, run_id: $run}}')"
# Endpoints listening:
# slack-notifier.yourdomain.com/hooks → handles build.*
# deploy-dashboard.yourdomain.com/api → handles build.completedPayment retry with dead-letter handling
When Stripe fires a payment_intent.payment_failed to your ingest endpoint, proxy it into DevOpsil. If your dunning service is unreachable, the event retries over 24 hours rather than being silently dropped. Use the delivery audit log to diagnose failures.
// Your Stripe webhook receiver — proxy into DevOpsil for reliable fanout
app.post("/stripe/webhooks", express.raw({ type: "application/json" }), async (req, res) => {
const event = stripe.webhooks.constructEvent(req.body, req.headers["stripe-signature"], process.env.STRIPE_SECRET);
// Forward to DevOpsil for reliable delivery to downstream services
await fetch("https://devopsil.com/api/v1/webhooks/events", {
method: "POST",
headers: { Authorization: "Bearer " + process.env.DEVOPSIL_WEH_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
type: event.type, // "payment_intent.payment_failed"
data: event.data.object,
idempotency_key: event.id, // Stripe event ID prevents double-delivery
}),
});
res.json({ received: true });
});