DevOpsil

API Documentation

All APIs use Bearer token authentication. Get your API key

Language:

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.

PrefixProductExample key (truncated)
wehWebhook Reliabilitydops_live_weh_a1b2c3d4...
secSecrets Leak Detectiondops_live_sec_e5f6a7b8...
piiPII Scrubbingdops_live_pii_c9d0e1f2...
depDeployment Risk Scoredops_live_dep_a3b4c5d6...
ratDistributed Rate Limitingdops_live_rat_e7f8a9b0...
apiAPI Health Monitordops_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 product

How to rotate keys

Create a new key in the dashboard before revoking the old one so you have zero-downtime rotation:

  1. Go to /dashboard/api-keys and create a new key for the same product.
  2. Deploy your application with the new key in your environment / secrets manager.
  3. Verify traffic is flowing successfully with the new key.
  4. Return to the dashboard and revoke the old key.

Errors

All errors return JSON with an error string. Some errors include additional fields.

StatusMeaning
401Missing or invalid Authorization header / key revoked
403Key exists but is not authorized for this product
400Invalid request body or parameters
404Resource not found or not owned by your account
422Request is valid JSON but cannot be processed (e.g. unsupported URL scheme)
429Per-minute rate limit or monthly quota exceeded
500Internal 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.

HeaderDescription
X-RateLimit-LimitPer-minute burst limit for your billing tier
X-RateLimit-RemainingRemaining burst calls in the current 60-second window
X-RateLimit-ResetUnix timestamp when the burst window resets
X-Quota-UsedTotal API calls consumed in the current billing period
X-Quota-LimitTotal API calls allowed in the current billing period
Retry-AfterSeconds to wait before retrying (only present on 429 responses)
TierMonthlyPer minute
Free50010
Developer10,00060
Starter50,000120
Team200,000300
Business1,000,000600
EnterpriseUnlimited3,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:

PatternModeMatches
*WildcardEvery event regardless of type
order.*Prefix wildcardorder.created, order.failed, order.refunded, etc.
order.createdExactOnly 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.

AttemptDelay after previous attemptTotal elapsed
1Immediate0s
21 second~1s
35 seconds~6s
430 seconds~36s
55 minutes~5m 36s
630 minutes~35m 36s
72 hours~2h 35m
824 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.completed

Payment 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 });
});