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

# Webhooks

> Get notified when users connect accounts and memories are indexed

Webhooks let your integration receive real-time updates from Hyperspell. Whenever a user connects a new account or a memory finishes indexing, Hyperspell will send a secure HTTP POST request to your webhook endpoint.

To receive webhooks, navigate to the [Dashboard](https://app.hyperspell.com/settings) and enter a webhook URL. You can choose which events to receive — connection notifications, indexing updates, or both. It is important that your webhook endpoint returns a 200 OK response to Hyperspell's request.

## Events

Hyperspell sends webhooks for the following events:

| Event                | Description                                                           |
| -------------------- | --------------------------------------------------------------------- |
| `connection-created` | A user has connected a new account (e.g. Notion, Google Drive, Slack) |
| `index-started`      | A memory has started indexing                                         |
| `index-completed`    | A memory has finished indexing                                        |

## Webhook payload

You will receive a JSON payload with the following fields:

<ParamField body="event" type="string" required>
  The event that triggered the webhook. One of `connection-created`, `index-started`, or `index-completed`.
</ParamField>

<ParamField body="app" type="string" required>
  The app slug that triggered the webhook.
</ParamField>

<ParamField body="user_id" type="string">
  The user id that triggered the webhook. Maybe be `null`.
</ParamField>

<ParamField body="source" type="string" required>
  The source that triggered the webhook, ie. `notion`, `slack`, `web_crawler`, etc.
</ParamField>

<ParamField body="resource_id" type="string">
  The resource id that triggered the webhook. Present on `index-started` and `index-completed` events. For the `web_crawler` source, this is the URL that started the crawl.
</ParamField>

<ParamField body="connection_id" type="string">
  The connection id. Present on `connection-created` events.
</ParamField>

<ParamField body="integration_id" type="string">
  The integration id. Present on `connection-created` events.
</ParamField>

<ParamField body="timestamp" type="int">
  The timestamp of the event in seconds since the Unix epoch.
</ParamField>

Additionally, the following headers are included in the request:

<ParamField header="X-Hyperspell-Signature" type="string">
  A SHA256 hash of the webhook payload using the app secret as the key. You can verify the hash to ensure the request is from Hyperspell.
</ParamField>

## Verifying the webhook

To verify that the webhook is coming from Hyperspell, you can use the `X-Hyperspell-Signature` header. This is a SHA256 hash of the webhook payload using the app's secret as the key. You can get the secret from the [Dashboard](https://app.hyperspell.com/api-keys) (it's the same as the `JWT Secret`)

You can verify the hash to ensure the request is from Hyperspell. Here's an example of how to receive and verify the webhook:

<CodeGroup>
  ```python Python theme={null}
  from fastapi import FastAPI, Request
  import hashlib
  import hmac

  def verify_webhook(payload, secret):
      hash = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
      return hmac.compare_digest(signagture, hash)

  class WebhookPayload(pydantic.BaseModel):
      event: str
      app: str
      user_id: str | None
      source: str
      resource_id: str | None = None
      connection_id: str | None = None
      integration_id: str | None = None
      timestamp: int

  app = FastAPI()

  @router.post("/webhooks/hyperspell")
  async def hyperspell_webhook(query: WebhookPayload, request: Request):
      # Verify that the request is from Hyperspell
      signature = request.headers.get("X-Hyperspell-Signature")
      if not verify_webhook(query, HYPERSPELL_JWT_SECRET):
          raise HTTPException(status_code=401, detail="Invalid signature")

      # Protect against replay attacks
      if query.timestamp < time.time() - 300:
          raise HTTPException(status_code=401, detail="Timestamp is too old")

      # Handle the webhook
      if query.event == "connection-created":
          # A user connected a new account
          ...
      elif query.event == "index-started":
          # A memory started indexing
          ...
      elif query.event == "index-completed":
          # A memory finished indexing
          ...

      return {"message": "Webhook received"}
  ```

  ```typescript TypeScript theme={null}
  // app/api/webhooks/hyperspell/route.ts
  import { NextResponse } from "next/server";
  import crypto from "node:crypto";
  export const runtime = "nodejs";

  type WebhookPayload = {
    event: "connection-created" | "index-started" | "index-completed" | string;
    app: string;
    user_id?: string | null;
    source: string;
    resource_id?: string | null;
    connection_id?: string | null;
    integration_id?: string | null;
    timestamp: number; // unix seconds
  };

  function verify_webhook(payload: WebhookPayload, secret: string) {
    const expected = crypto.createHmac("sha256", secret).update(payload).digest("hex");
    return hmac.compare_digest(sig, expected);
  }

  export async function POST(req: Request) {
    const secret = process.env.HYPERSPELL_WEBHOOK_SECRET!;
    const sig = (req.headers.get("x-hyperspell-signature") || "").toLowerCase();
    const raw = Buffer.from(await req.arrayBuffer());

    if (!verify_webhook(payload, secret)) return NextResponse.json({ error: "invalid_signature" }, { status: 401 });

    const payload = JSON.parse(raw.toString("utf8")) as WebhookPayload;

    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - payload.timestamp) > 300) { // 5 minutes
      return NextResponse.json({ error: "stale_or_future" }, { status: 401 });
    }

    switch (payload.event) {
      case "connection-created":
        // a user connected a new account
        break;
      case "index-started":
        // a memory started indexing
        break;
      case "index-completed":
        // a memory finished indexing
        break;
    }

    return NextResponse.json({ ok: true });
  }
  ```
</CodeGroup>
