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

# Verify webhook signatures with the SDK, per language

> Copy-pasteable webhook verification handlers for every server-side SDK — Node, Python, Go, PHP, Ruby, Java, and C# — each one a complete framework handler that reads the raw body, verifies, and answers 401 on failure.

# Verify webhook signatures with the SDK, per language

Verifying a webhook signature is one SDK call in every language. What trips teams up is the wiring around it: reading the raw body before a JSON parser mutates it, reading the right header, and returning `401` (not `200`) on a bad signature so retries don't ack forged traffic. This page gives you a complete, working handler per language — paste it, point it at your secret, and it runs.

The protocol itself (header formats, the `<t>.<raw_body>` signed string, rotation grace windows, timing-safe comparison) is specified on [Webhook security](/webhooks/security). If you have a Python or Go backend and don't want to pull in the SDK, the [no-SDK verifiers](/guides/webhook-signature-verify-polyglot) do the same check with the standard library.

## What every handler does

1. **Read the raw body** — before any JSON middleware or parser runs. HMAC is over the exact bytes Orbit POSTed; a parse-and-reserialize changes them.
2. **Read the signature header** — canonical `X-Orbit-Signature` first, legacy `X-Devotel-Signature` as fallback (old queued deliveries may carry only the legacy one). Each carries `t=<unix_ts>,v1=<hex>`.
3. **Verify** — one SDK call: returns the decoded event, throws on any failure (malformed header, replay-window breach, signature mismatch, invalid JSON).
4. **Answer `401` on failure** — a forged or stale request must not get `200`.

## Node.js (Express)

SDK: `Orbit.webhooks.constructEvent(payload, signature, secret)` — throws `OrbitWebhookSignatureError` on failure. Mount Express's raw body parser on the webhook route only; keep your JSON parser elsewhere.

```typescript theme={null}
import express from 'express';
import { Orbit, OrbitWebhookSignatureError } from '@devotel-orbit/node';

const app = express();

app.post(
  '/webhooks/orbit',
  express.raw({ type: 'application/json' }), // raw bytes, not parsed JSON
  (req, res) => {
    const signature =
      (req.headers['x-orbit-signature'] as string | undefined) ??
      (req.headers['x-devotel-signature'] as string | undefined) ??
      '';
    try {
      const event = Orbit.webhooks.constructEvent(
        req.body.toString('utf8'),
        signature,
        process.env.ORBIT_WEBHOOK_SECRET!,
      );
      switch (event.type) {
        case 'message.delivered':
          // handle the event...
          break;
        default:
          break;
      }
      res.status(200).json({ received: true });
    } catch (err) {
      if (err instanceof OrbitWebhookSignatureError) {
        return res.status(401).json({ error: err.message });
      }
      throw err;
    }
  },
);
```

## Python (Flask)

SDK: `verify_webhook(payload=..., signature=..., secret=...)` — keyword-only, returns the decoded event dict, raises `OrbitWebhookSignatureError`.

```python theme={null}
from flask import Flask, request, jsonify
from orbit_sdk import verify_webhook
from orbit_sdk.errors import OrbitWebhookSignatureError

app = Flask(__name__)

@app.route("/webhooks/orbit", methods=["POST"])
def handle_webhook():
    signature = (
        request.headers.get("X-Orbit-Signature")
        or request.headers.get("X-Devotel-Signature")
        or ""
    )
    try:
        event = verify_webhook(
            payload=request.get_data(),  # raw bytes — don't parse first
            signature=signature,
            secret="whsec_your_secret",
        )
    except OrbitWebhookSignatureError as exc:
        return jsonify({"error": str(exc)}), 401
    # handle event ...
    return jsonify({"received": True}), 200
```

## Go (`net/http`)

SDK: `orbit.VerifyWebhook(payload, signatureHeader, secret, 0, time.Time{})` — the `0` tolerance picks the default 5-minute window; the zero `time.Time{}` means "now". Returns the decoded event or a `*orbit.WebhookSignatureError`.

```go theme={null}
package main

import (
    "io"
    "net/http"
    "time"

    orbit "github.com/devotel/orbit-go/orbit"
)

func handler(w http.ResponseWriter, r *http.Request) {
    rawBody, err := io.ReadAll(r.Body) // raw bytes; reuse for verify + JSON
    if err != nil {
        http.Error(w, "read error", http.StatusBadRequest)
        return
    }
    signature := r.Header.Get("X-Orbit-Signature")
    if signature == "" {
        signature = r.Header.Get("X-Devotel-Signature")
    }
    event, err := orbit.VerifyWebhook(
        rawBody,
        signature,
        "whsec_your_secret",
        0,            // default tolerance
        time.Time{},  // now
    )
    if err != nil {
        http.Error(w, `{"error":"invalid signature"}`, http.StatusUnauthorized)
        return
    }
    _ = event // handle the event...
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"received":true}`))
}

func main() {
    http.HandleFunc("/webhooks/orbit", handler)
    http.ListenAndServe(":8080", nil)
}
```

## PHP

SDK: `Webhooks::verify(payload: ..., signature: ..., secret: ...)` — named arguments, returns the decoded event array, throws `OrbitWebhookSignatureError`.

```php theme={null}
<?php

use Devotel\Orbit\Webhooks;
use Devotel\Orbit\Errors\OrbitWebhookSignatureError;

$signature = $_SERVER['HTTP_X_ORBIT_SIGNATURE']
    ?? $_SERVER['HTTP_X_DEVOTEL_SIGNATURE']
    ?? '';

try {
    $event = Webhooks::verify(
        payload: file_get_contents('php://input'), // raw body
        signature: $signature,
        secret: getenv('ORBIT_WEBHOOK_SECRET'),
    );
} catch (OrbitWebhookSignatureError $e) {
    http_response_code(401);
    echo json_encode(['error' => $e->getMessage()]);
    exit;
}

// handle $event ...
http_response_code(200);
echo json_encode(['received' => true]);
```

## Ruby (Sinatra)

SDK: `OrbitSdk::Webhooks.verify(payload:, signature:, secret:)` — keyword arguments, returns the decoded event hash, raises `OrbitSdk::OrbitWebhookSignatureError`.

```ruby theme={null}
require "sinatra"
require "orbit_sdk"

post "/webhooks/orbit" do
  signature = request.env["HTTP_X_ORBIT_SIGNATURE"] ||
              request.env["HTTP_X_DEVOTEL_SIGNATURE"] || ""
  begin
    event = OrbitSdk::Webhooks.verify(
      payload: request.body.read, # raw body
      signature: signature,
      secret: ENV["ORBIT_WEBHOOK_SECRET"]
    )
    # handle event ...
    status 200
    { received: true }.to_json
  rescue OrbitSdk::OrbitWebhookSignatureError => e
    status 401
    { error: e.message }.to_json
  end
end
```

## Java

SDK: `Webhooks.verify(payload, signature, secret)` — returns the decoded event `Map`, throws `OrbitWebhookSignatureError`. The example below runs on the JDK's built-in `HttpServer`; with Spring or another framework, capture the body as raw `byte[]`/String before any deserializer touches it.

```java theme={null}
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import io.devotel.orbit.Webhooks;
import io.devotel.orbit.errors.OrbitWebhookSignatureError;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Map;

public class WebhookReceiver {
    public static void main(String[] args) throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
        server.createContext("/webhooks/orbit", WebhookReceiver::handle);
        server.start();
    }

    private static void handle(HttpExchange exchange) {
        try {
            byte[] raw = exchange.getRequestBody().readAllBytes(); // raw body
            String signature = exchange.getRequestHeaders()
                    .getFirst("X-Orbit-Signature");
            if (signature == null) {
                signature = exchange.getRequestHeaders()
                        .getFirst("X-Devotel-Signature");
            }
            try {
                Map<String, Object> event = Webhooks.verify(
                        new String(raw, StandardCharsets.UTF_8),
                        signature,
                        System.getenv("ORBIT_WEBHOOK_SECRET"));
                // handle event ...
                respond(exchange, 200, "{\"received\":true}");
            } catch (OrbitWebhookSignatureError e) {
                respond(exchange, 401, "{\"error\":\"invalid signature\"}");
            }
        } catch (Exception e) {
            respondQuiet(exchange, 500);
        }
    }

    private static void respond(HttpExchange exchange, int status, String body) throws java.io.IOException {
        byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
        exchange.getResponseHeaders().set("Content-Type", "application/json");
        exchange.sendResponseHeaders(status, bytes.length);
        exchange.getResponseBody().write(bytes);
        exchange.close();
    }

    private static void respondQuiet(HttpExchange exchange, int status) {
        try {
            exchange.sendResponseHeaders(status, -1);
            exchange.close();
        } catch (java.io.IOException ignored) {}
    }
}
```

## C# (ASP.NET minimal API)

SDK: `Webhooks.Verify(payload, signature, secret)` — returns the decoded event `JsonElement`, throws `OrbitWebhookSignatureError`.

```csharp theme={null}
using Devotel.Orbit; // Webhooks, OrbitWebhookSignatureError

var app = WebApplication.Create(args);

app.MapPost("/webhooks/orbit", async (HttpRequest request) =>
{
    using var reader = new StreamReader(request.Body);
    var payload = await reader.ReadToEndAsync(); // raw body
    var signature =
        request.Headers["X-Orbit-Signature"].FirstOrDefault()
        ?? request.Headers["X-Devotel-Signature"].FirstOrDefault()
        ?? string.Empty;
    try
    {
        var parsed = Webhooks.Verify(
            payload,
            signature,
            Environment.GetEnvironmentVariable("ORBIT_WEBHOOK_SECRET") ?? "");
        // handle parsed ...
        return Results.Json(new { received = true });
    }
    catch (OrbitWebhookSignatureError ex)
    {
        return Results.Json(
            new { error = ex.Message },
            statusCode: StatusCodes.Status401Unauthorized);
    }
});

app.Run();
```

## Rotation and troubleshooting

During a secret rotation grace window, verify `X-Orbit-Signature` against the current secret and `X-Orbit-Signature-Next` against the previous secret — both headers arrive on every delivery until the grace window ends (7 days). The pattern per language mirrors the rotation section of the [no-SDK guide](/guides/webhook-signature-verify-polyglot#rotation-handling-x-orbit-signature-next).

If a valid delivery still fails verification, the cause is almost always one of: a body parser ran before verification (fix per Step 1 above), a masked secret preview (`whsec_********...<last4>`) copied in instead of a live secret, or server clock skew past the replay window. See [Troubleshooting: signature verification failures](/webhooks/troubleshooting-signature-failures).

## Continue

* [Webhook security](/webhooks/security) — full signature protocol
* [Verify without the SDK (Python and Go)](/guides/webhook-signature-verify-polyglot) — stdlib verifiers
* [Webhook events](/webhooks/events) — event catalog
* [Build a durable webhook consumer](/guides/webhook-consumer) — queue, dedupe, ack-fast pattern
