> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getruba.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a Reliable Webhook Receiver

> Verify signatures, acknowledge quickly, and recover from delivery failures

export const RubaSocials = () => {
  if (typeof document === "undefined") {
    return null;
  }
  const inject = () => {
    const footer = document.getElementById("footer");
    if (!footer || document.getElementById("ruba-socials")) {
      return false;
    }
    const isDark = document.querySelector("html.dark") || document.querySelector('html[class*="dark"]');
    const lineColor = isDark ? "#383838" : "#e8e8ed";
    const iconColor = isDark ? "#8e8e93" : "#86868b";
    const markColor = isDark ? "#ffffff" : "#08080c";
    const hoverColor = "#0071e3";
    const div = document.createElement("div");
    div.id = "ruba-socials";
    div.style.cssText = `display:flex;align-items:center;gap:20px;padding-top:20px;margin-top:20px;border-top:1px solid ${lineColor};width:100%`;
    const logo = `<a href="https://getruba.com" style="display:inline-flex;height:24px;align-items:center;gap:8px;text-decoration:none;color:${markColor};line-height:1;"><svg width="24" height="24" viewBox="0 0 120 120" aria-hidden="true" style="display:block;flex:none;"><g transform="translate(2.5 0)"><path d="M19 25h59L65.43 47H19a4 4 0 0 1-4-4V29a4 4 0 0 1 4-4Z" fill="${markColor}"/><path d="M19 73h31.57L38 95H19a4 4 0 0 1-4-4V77a4 4 0 0 1 4-4Z" fill="${markColor}"/><path d="M85 25h25L70 95H45Z" fill="#007AFF"/></g></svg><span style="display:inline-flex;height:24px;align-items:center;font-weight:600;font-size:15px;color:${markColor};letter-spacing:-0.01em;line-height:1;">Ruba</span></a>`;
    const spacer = `<div style="flex:1"></div>`;
    const ig = `<a href="https://instagram.com/getruba" target="_blank" rel="noopener" aria-label="Instagram" style="display:flex;align-items:center;color:${iconColor};transition:color 0.15s ease;" onmouseover="this.style.color='${hoverColor}'" onmouseout="this.style.color='${iconColor}'"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="20" rx="5" ry="5"/><path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"/><line x1="17.5" y1="6.5" x2="17.51" y2="6.5"/></svg></a>`;
    const li = `<a href="https://linkedin.com/company/getruba" target="_blank" rel="noopener" aria-label="LinkedIn" style="display:flex;align-items:center;color:${iconColor};transition:color 0.15s ease;" onmouseover="this.style.color='${hoverColor}'" onmouseout="this.style.color='${iconColor}'"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"/><rect x="2" y="9" width="4" height="12"/><circle cx="4" cy="4" r="2"/></svg></a>`;
    const x = `<a href="https://x.com/getruba" target="_blank" rel="noopener" aria-label="X" style="display:flex;align-items:center;color:${iconColor};transition:color 0.15s ease;" onmouseover="this.style.color='${hoverColor}'" onmouseout="this.style.color='${iconColor}'"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg></a>`;
    const github = `<a href="https://github.com/rubadot" target="_blank" rel="noopener" aria-label="GitHub" style="display:flex;align-items:center;color:${iconColor};transition:color 0.15s ease;" onmouseover="this.style.color='${hoverColor}'" onmouseout="this.style.color='${iconColor}'"><svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 .7a11.5 11.5 0 0 0-3.64 22.4c.58.1.79-.25.79-.56v-2.24c-3.22.7-3.9-1.37-3.9-1.37-.53-1.34-1.29-1.7-1.29-1.7-1.05-.72.08-.71.08-.71 1.16.08 1.78 1.2 1.78 1.2 1.04 1.77 2.72 1.26 3.38.96.1-.75.4-1.26.74-1.55-2.57-.3-5.27-1.29-5.27-5.69 0-1.26.45-2.29 1.19-3.09-.12-.29-.52-1.47.11-3.05 0 0 .97-.31 3.16 1.18A10.96 10.96 0 0 1 12 6.09c.98 0 1.94.13 2.86.39 2.2-1.49 3.16-1.18 3.16-1.18.63 1.58.23 2.76.11 3.05.74.8 1.19 1.83 1.19 3.09 0 4.42-2.7 5.39-5.28 5.68.42.36.79 1.06.79 2.14v3.28c0 .31.21.67.8.56A11.5 11.5 0 0 0 12 .7Z"/></svg></a>`;
    div.innerHTML = logo + spacer + ig + li + x + github;
    footer.appendChild(div);
    return true;
  };
  if (!document.documentElement.dataset.rubaSocialsObserver) {
    document.documentElement.dataset.rubaSocialsObserver = "true";
    const observer = new MutationObserver(() => {
      if (!document.getElementById("ruba-socials")) {
        inject();
      }
    });
    observer.observe(document.body, {
      childList: true,
      subtree: true
    });
  }
  setTimeout(() => {
    if (!inject()) {
      const interval = setInterval(() => {
        if (inject()) {
          clearInterval(interval);
        }
      }, 500);
      setTimeout(() => clearInterval(interval), 10000);
    }
  }, 300);
  return null;
};

<link rel="stylesheet" href="/style.css" />

<RubaSocials />

<img className="block dark:hidden" src="https://mintcdn.com/ruba/pmKQW7SekAkxQqgn/assets/integrate/webhooks/delivery.light.png?fit=max&auto=format&n=pmKQW7SekAkxQqgn&q=85&s=7bfe8819b532b4b1063a5acee5a9c988" width="2740" height="1522" data-path="assets/integrate/webhooks/delivery.light.png" />

<img className="hidden dark:block" src="https://mintcdn.com/ruba/JDRjNJtBhntQypch/assets/integrate/webhooks/delivery.dark.png?fit=max&auto=format&n=JDRjNJtBhntQypch&q=85&s=16f1ca9d38bc672737d4286023aa5f96" width="2672" height="1526" data-path="assets/integrate/webhooks/delivery.dark.png" />

The delivery view records past attempts, request payloads, failure details, and manual redelivery. Your receiver still needs to preserve the raw body, verify the signature, queue durable work, and return success quickly.

## Receiver sequence

```text theme={"system"}
Ruba request → raw body + headers → signature verification → durable queue → 2xx response
```

### Verify with an SDK

Set `RUBA_WEBHOOK_SECRET` to the endpoint secret. Reject verification failures and acknowledge accepted events with a 2xx response.

<CodeGroup>
  ```typescript icon="square-js" TypeScript with Express theme={"system"}
  import express, { Request, Response } from 'express'
  import { validateEvent, WebhookVerificationError } from '@getruba/sdk/webhooks'

  const app = express()

  app.post('/webhook', express.raw({ type: 'application/json' }), (req: Request, res: Response) => {
    try {
      const event = validateEvent(
        req.body,
        req.headers,
        process.env['RUBA_WEBHOOK_SECRET'] ?? '',
      )

      // Queue event for processing.
      res.status(202).send('')
    } catch (error) {
      if (error instanceof WebhookVerificationError) {
        return res.status(403).send('')
      }
      throw error
    }
  })
  ```

  ```python Python with Flask theme={"system"}
  import os
  from flask import Flask, request
  from ruba.webhooks import validate_event, WebhookVerificationError

  app = Flask(__name__)

  @app.route('/webhook', methods=['POST'])
  def webhook():
      try:
          event = validate_event(
              body=request.data,
              headers=request.headers,
              secret=os.getenv('RUBA_WEBHOOK_SECRET', ''),
          )
          # Queue event for processing.
          return "", 202
      except WebhookVerificationError:
          return "", 403
  ```
</CodeGroup>

### Verify without a Ruba SDK

Ruba follows [Standard Webhooks](https://www.standardwebhooks.com/). Use an available [language library](https://github.com/standard-webhooks/standard-webhooks/tree/main/libraries) or implement the [specification](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md). A manual implementation must base64-encode the endpoint secret before signature generation; Ruba SDK helpers handle that detail.

## Network allowlist

Production and sandbox currently use the same sources:

```text theme={"system"}
3.134.238.10
3.129.111.220
52.15.118.168
74.220.50.0/24
74.220.58.0/24
```

The final two ranges were added on October 27, 2025.

## Delivery contract

| Condition                             | Ruba behavior                                             | Receiver design                                                                                     |
| ------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| Network error or non-success response | Up to 10 attempts with exponential backoff                | Make processing idempotent                                                                          |
| No response within 10 seconds         | Attempt is timed out and retried                          | Queue work and answer within 2 seconds                                                              |
| 10 consecutive non-2xx deliveries     | Endpoint is disabled and an organization admin is emailed | Fix it, then re-enable it in [webhook settings](https://getruba.com/to/dashboard/settings/webhooks) |
| 3xx response                          | Treated as failure; redirects are not followed            | Register the final URL                                                                              |

## Diagnose a missing event

1. Confirm the delivery appears in Ruba and inspect its status and payload.
2. During local work, confirm the tunnel is running.
3. Log entry, verification start, verification success, and queue insertion separately.
4. Send `curl -vvv -X POST <endpoint>` to expose a missing route or redirect. Some frameworks distinguish `/foo` from `/foo/`.
5. Exclude the public webhook route from login middleware.

For a `403`, inspect reverse-proxy and Cloudflare firewall logs. Cloudflare Bot Fight Mode can block valid Ruba requests even when the source IP is allowlisted; disable that mode for the receiver or adjust the relevant security configuration. For signature errors, verify that the exact raw request body is used and that a custom implementation base64-encodes the secret.
