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

# Webhooks

> Deliver Rangler company, filing, fund, meeting, and Connect events to your systems with signed webhook requests.

Webhooks deliver matching Rangler events to your systems.

Use webhooks as the default way to receive product events. Use event feeds to load earlier events and check for missed deliveries.

## Delivery model

Rangler sends HTTPS `POST` requests to every active webhook endpoint in your organization that matches at least one active event subscription rule.

The portal supports multiple active endpoints per organization. Rangler evaluates subscriptions at the organization level, then fans matching events out to each active endpoint for that organization.

The portal lists all supported market-event types, including filings, financial results, corporate actions, board changes, company meetings and streams, and fund updates. Connect has a separate set of event types for connection and synchronization updates.

The payload follows the webhook event format documented in [Events](/events).

## Send a managed test event

Create an endpoint in the portal, then send a test event from its endpoint actions. You can make the same request through the portal API:

```http theme={null}
POST /developer/v1/organizations/{org_id}/event-destinations/{destination_id}/test-events
Authorization: Bearer <portal_token>
Content-Type: application/json

{"event_type":"filing.new"}
```

The request returns `202 Accepted` after Rangler queues the event with the managed delivery service. Test events appear in endpoint delivery history, so you can inspect status, response code, latency, and failure classification.

Supported managed test types are currently `filing.new` and `fund.disclosure.updated`. They validate transport and signing; they are not inserted into the public `/v1/events` feed.

Example test payload:

```json theme={null}
{
  "id": "2b2dc6f4-48aa-4fd7-9d0a-6c96b9f1e4ea",
  "type": "filing.new",
  "occurred_at": "2026-04-11T11:05:12.193927Z",
  "organization_id": "6a4ce6d5-1a8f-4cb3-b5cc-070d46fe6f94",
  "company_id": null,
  "fund_id": null,
  "source_kind": "filing",
  "source_id": "rangler:test:6a4ce6d5-1a8f-4cb3-b5cc-070d46fe6f94:2b2dc6f4-48aa-4fd7-9d0a-6c96b9f1e4ea",
  "title": "Sandbox filing published",
  "summary": "A sandbox filing event was generated from the Rangler portal.",
  "data": {
    "sandbox": true,
    "generated_by": "rangler_portal"
  }
}
```

## Headers

Every webhook delivery includes `Webhook-Id`, `Webhook-Timestamp`, and `Webhook-Signature`.

`Webhook-Id` participates in signature verification and identifies the managed transport event. The payload's top-level `id` identifies the logical Rangler event and is the public key for duplicate-processing protection. Do not assume the two values are interchangeable.

Rangler signs the raw request body using:

```text theme={null}
<webhook_id>.<webhook_timestamp>.<raw_body>
```

The signature header format is:

```text theme={null}
v1,<base64_hmac_sha256_digest>
```

## Signature verification examples

Use the raw request body exactly as received.

<Tabs>
  <Tab title="Node.js">
    ```ts theme={null}
    import crypto from 'node:crypto';

    export function verifyRanglerWebhook({
      rawBody,
      secret,
      webhookId,
      webhookTimestamp,
      signature,
      toleranceSeconds = 300,
    }: {
      rawBody: Buffer;
      secret: string;
      webhookId: string;
      webhookTimestamp: string;
      signature: string;
      toleranceSeconds?: number;
    }) {
      const timestampSeconds = Number(webhookTimestamp);
      if (!Number.isInteger(timestampSeconds)) return false;
      if (Math.abs(Date.now() / 1000 - timestampSeconds) > toleranceSeconds) return false;

      const signedPayload = Buffer.concat([
        Buffer.from(webhookId, 'utf8'),
        Buffer.from('.', 'utf8'),
        Buffer.from(webhookTimestamp, 'utf8'),
        Buffer.from('.', 'utf8'),
        rawBody,
      ]);

      const digest = crypto.createHmac('sha256', decodeSecret(secret)).update(signedPayload).digest('base64');
      const expected = Buffer.from(`v1,${digest}`);
      const received = Buffer.from(signature);
      return expected.length === received.length && crypto.timingSafeEqual(expected, received);
    }

    function decodeSecret(secret: string) {
      if (!secret.startsWith('whsec_')) {
        return Buffer.from(secret, 'utf8');
      }
      const encoded = secret.slice('whsec_'.length);
      const padded = encoded + '='.repeat((4 - (encoded.length % 4)) % 4);
      return Buffer.from(padded, 'base64url');
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import base64
    import hmac
    from hashlib import sha256
    import time


    def verify_rangler_webhook(
        *,
        raw_body: bytes,
        secret: str,
        webhook_id: str,
        webhook_timestamp: str,
        signature: str,
        tolerance_seconds: int = 300,
    ) -> bool:
        try:
            timestamp_seconds = int(webhook_timestamp)
        except ValueError:
            return False
        if abs(time.time() - timestamp_seconds) > tolerance_seconds:
            return False

        signed_payload = b".".join(
            [
                webhook_id.encode("utf-8"),
                webhook_timestamp.encode("utf-8"),
                raw_body,
            ]
        )
        digest = hmac.new(_decode_secret(secret), signed_payload, sha256).digest()
        expected = f"v1,{base64.b64encode(digest).decode('utf-8')}"
        return hmac.compare_digest(expected, signature)


    def _decode_secret(secret: str) -> bytes:
        if not secret.startswith("whsec_"):
            return secret.encode("utf-8")
        encoded = secret[len("whsec_") :]
        padded = encoded + ("=" * ((4 - len(encoded) % 4) % 4))
        return base64.urlsafe_b64decode(padded.encode("utf-8"))
    ```
  </Tab>
</Tabs>

The examples reject signatures older or newer than five minutes. Apply the same timestamp check in production to limit replay attacks.

## Receiver requirements

Your receiver should accept JSON payloads over HTTPS and use this order of operations:

1. Read and preserve the raw request body.
2. Verify `Webhook-Signature`, including the signed `Webhook-Id` and `Webhook-Timestamp`.
3. Parse the payload and atomically insert its top-level event `id` into a durable inbox with a uniqueness constraint.
4. Return `2xx` after the inbox record is committed.
5. Process pending inbox records asynchronously and record their processing state.

If the event `id` already exists, return `2xx` without applying its business effects again. Use a database-backed inbox or another shared durable store in production; an in-memory set cannot coordinate replicas and is erased by restarts.

## Retries

Rangler treats any non-`2xx` response, timeout, or connection failure as a failed attempt.

Webhook delivery is at least once. Managed delivery retries use backoff and do not guarantee ordering. Check the portal's delivery status and `next_attempt_at` value instead of hard-coding a retry timetable in your integration.

Duplicate delivery is expected behavior, not evidence that Rangler created the underlying event twice. Deduplicate by the payload event `id` and design business processing so a worker retry cannot repeat an irreversible side effect.

Treat the webhook request as a notification, not as proof that work has completed in your system.

Recent delivery attempts are visible for each endpoint in the Rangler Portal. You can redeliver failed attempts from the portal or through the portal API.

## Sandbox testing

Rangler supports managed test events and delivery-attempt inspection from the portal.

See [Sandbox Testing](/sandbox-testing) for the recommended flow.

## Recommended architecture

<Steps>
  <Step>
    ### Rangler sends a webhook event

    The delivery arrives as a signed HTTPS `POST` request in the documented webhook format.
  </Step>

  <Step>
    ### Your edge receiver verifies the signature

    Validate the signature against the raw request body before doing any downstream work.
  </Step>

  <Step>
    ### Commit the event to a durable inbox

    Uniquely insert the payload event `id` and payload. Acknowledge only after that durable write commits; workers can then process the inbox asynchronously.
  </Step>

  <Step>
    ### Fetch any supporting API data

    Pull filing, fund, or entity detail if your workflow needs richer context than the webhook payload carries.
  </Step>

  <Step>
    ### Fan out to your application

    Send alerts, update workflow state, or trigger internal automations after the event is accepted.
  </Step>
</Steps>

## Security expectations

Terminate TLS correctly, verify the Rangler signature on every request, reject requests with invalid signatures, use distinct receiver URLs or endpoint configs for test validation and live traffic, and rotate webhook credentials and API keys through normal operational processes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.