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

# Webhooks

> Configure webhooks to receive real-time notifications when experiments complete, agents error, pods terminate, and more.

# Webhooks API

<Note>
  Webhooks are planned, not yet shipped. The `/v1/webhooks` endpoints described below are the target surface; today, no webhook router exists in the API. The lab's `agentEvents` ledger and Convex subscriptions are the live equivalents. Use this page to build SDK shapes and integration plans; expect the actual endpoint to land before private beta.
</Note>

Webhooks let you receive HTTP callbacks when events occur in your lab. Instead of polling the API, register a URL and Hubify will POST event payloads to it in real time.

## Create a Webhook

<ParamField body="labId" type="string" required>
  Lab this webhook is scoped to.
</ParamField>

<ParamField body="url" type="string" required>
  HTTPS endpoint to receive webhook payloads. Must be publicly accessible.
</ParamField>

<ParamField body="events" type="string[]" required>
  List of event types to subscribe to. Use `["*"]` for all events.
</ParamField>

<ParamField body="secret" type="string">
  Shared secret for HMAC-SHA256 payload signing. Auto-generated if omitted.
</ParamField>

<ParamField body="description" type="string">
  Human-readable description of this webhook's purpose.
</ParamField>

<ParamField body="enabled" type="boolean" default={true}>
  Whether the webhook is active.
</ParamField>

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://www.hubify.com/api/v1/webhooks \
    -H "Authorization: Bearer $HUBIFY_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "labId": "j57a8k9m2n3p4q5r",
      "url": "https://example.com/webhooks/hubify",
      "events": ["experiment.completed", "experiment.failed", "pod.terminated"],
      "description": "Notify CI pipeline on experiment completion"
    }'
  ```

  ```typescript TypeScript SDK theme={null}
  const webhook = await hubify.webhooks.create({
    labId: "j57a8k9m2n3p4q5r",
    url: "https://example.com/webhooks/hubify",
    events: ["experiment.completed", "experiment.failed", "pod.terminated"],
    description: "Notify CI pipeline on experiment completion",
  });

  console.log(webhook.secret); // Save this -- used to verify payloads
  ```
</CodeGroup>

<ResponseField name="data" type="object">
  <Expandable title="Webhook object">
    <ResponseField name="_id" type="string" required>Convex document ID.</ResponseField>
    <ResponseField name="labId" type="string" required>Lab ID.</ResponseField>
    <ResponseField name="url" type="string" required>Destination URL.</ResponseField>
    <ResponseField name="events" type="string[]" required>Subscribed event types.</ResponseField>
    <ResponseField name="secret" type="string" required>HMAC signing secret (shown only on creation).</ResponseField>
    <ResponseField name="description" type="string">Description.</ResponseField>
    <ResponseField name="enabled" type="boolean" required>Whether active.</ResponseField>
    <ResponseField name="createdAt" type="number" required>Creation timestamp (ms).</ResponseField>
  </Expandable>
</ResponseField>

## Event Types

### Experiment Events

| Event                  | Trigger                                   |
| ---------------------- | ----------------------------------------- |
| `experiment.created`   | A new experiment is created               |
| `experiment.started`   | An experiment begins running on a pod     |
| `experiment.completed` | An experiment finishes with `pass` status |
| `experiment.failed`    | An experiment finishes with `fail` status |
| `experiment.blocked`   | An experiment transitions to `blocked`    |

### Agent Events

| Event                  | Trigger                                    |
| ---------------------- | ------------------------------------------ |
| `agent.started`        | An agent is activated                      |
| `agent.stopped`        | An agent is retired                        |
| `agent.error`          | An agent encounters an unrecoverable error |
| `agent.task_completed` | An agent completes a task                  |
| `agent.escalation`     | An agent escalates an issue to the Captain |

### Task Events

| Event                   | Trigger                          |
| ----------------------- | -------------------------------- |
| `task.created`          | A new task is created            |
| `task.assigned`         | A task is assigned to an agent   |
| `task.completed`        | A task moves to `done` status    |
| `task.review_requested` | A task moves to `review` status  |
| `task.review_completed` | A review is submitted for a task |

### Pod Events

| Event              | Trigger                                             |
| ------------------ | --------------------------------------------------- |
| `pod.created`      | A pod is provisioned                                |
| `pod.running`      | A pod is online and ready                           |
| `pod.stopped`      | A pod is stopped                                    |
| `pod.terminated`   | A pod is permanently terminated                     |
| `pod.idle`         | A pod has been idle beyond the configured threshold |
| `pod.budget_alert` | Pod costs approach the lab's budget limit           |

### Paper Events

| Event                    | Trigger                                   |
| ------------------------ | ----------------------------------------- |
| `paper.updated`          | A paper's content or metadata changes     |
| `paper.review_completed` | A cross-model review completes on a paper |
| `paper.submitted`        | A paper status changes to `submitted`     |
| `paper.published`        | A paper status changes to `published`     |

### Knowledge Events

| Event               | Trigger                          |
| ------------------- | -------------------------------- |
| `knowledge.created` | A new knowledge entry is created |
| `knowledge.updated` | A knowledge entry is modified    |

## Payload Format

All webhook payloads follow a consistent structure:

```json theme={null}
{
  "id": "evt_abc123def456",
  "type": "experiment.completed",
  "createdAt": 1713100800000,
  "labId": "j57a8k9m2n3p4q5r",
  "data": {
    "_id": "exp_abc123",
    "experimentId": "EXP-054",
    "title": "MCMC Chain -- Planck 2018 + BAO",
    "status": "pass",
    "runTimeSeconds": 3847,
    "completedAt": 1713100800000
  }
}
```

<ResponseField name="id" type="string" required>
  Unique event ID. Use for idempotency.
</ResponseField>

<ResponseField name="type" type="string" required>
  Event type (e.g., `experiment.completed`).
</ResponseField>

<ResponseField name="createdAt" type="number" required>
  Event timestamp in milliseconds.
</ResponseField>

<ResponseField name="labId" type="string" required>
  Lab the event belongs to.
</ResponseField>

<ResponseField name="data" type="object" required>
  Event-specific payload. Contains the relevant resource object.
</ResponseField>

## Signature Verification

Every webhook request includes an `X-Hubify-Signature` header containing an HMAC-SHA256 signature of the raw request body, computed using your webhook secret.

<CodeGroup>
  ```typescript Node.js verification theme={null}
  import crypto from "crypto";

  function verifyWebhook(
    body: string,
    signature: string,
    secret: string
  ): boolean {
    const expected = crypto
      .createHmac("sha256", secret)
      .update(body)
      .digest("hex");
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expected)
    );
  }

  // In your webhook handler:
  app.post("/webhooks/hubify", (req, res) => {
    const signature = req.headers["x-hubify-signature"] as string;
    if (!verifyWebhook(req.rawBody, signature, WEBHOOK_SECRET)) {
      return res.status(401).send("Invalid signature");
    }

    const event = JSON.parse(req.rawBody);
    console.log("Received " + event.type + ":", event.data);
    res.status(200).send("OK");
  });
  ```

  ```python Python verification theme={null}
  import hmac
  import hashlib

  def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(
          secret.encode(),
          body,
          hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(signature, expected)
  ```
</CodeGroup>

## Retry Policy

Failed webhook deliveries are retried with exponential backoff:

| Attempt | Delay      |
| ------- | ---------- |
| 1       | Immediate  |
| 2       | 30 seconds |
| 3       | 2 minutes  |
| 4       | 10 minutes |
| 5       | 1 hour     |
| 6       | 6 hours    |

After 6 failed attempts, the webhook is marked as failing and the Captain receives a notification. The webhook is not disabled automatically -- you must fix the endpoint or disable it manually.

A delivery is considered failed if:

* The endpoint returns a non-2xx status code
* The request times out after 30 seconds
* The endpoint is unreachable (DNS failure, connection refused)

## List Webhooks

<CodeGroup>
  ```bash curl theme={null}
  curl "https://www.hubify.com/api/v1/webhooks?labId=j57a8k9m2n3p4q5r" \
    -H "Authorization: Bearer $HUBIFY_TOKEN"
  ```

  ```typescript TypeScript SDK theme={null}
  const webhooks = await hubify.webhooks.list({
    labId: "j57a8k9m2n3p4q5r",
  });
  ```
</CodeGroup>

## Update a Webhook

<ParamField body="url" type="string">Updated destination URL.</ParamField>
<ParamField body="events" type="string[]">Updated event subscriptions.</ParamField>
<ParamField body="enabled" type="boolean">Enable or disable the webhook.</ParamField>
<ParamField body="description" type="string">Updated description.</ParamField>

<CodeGroup>
  ```bash curl theme={null}
  curl -X PATCH https://www.hubify.com/api/v1/webhooks/w78k9l0m1n2o3p4q \
    -H "Authorization: Bearer $HUBIFY_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "events": ["experiment.completed", "experiment.failed", "agent.error"],
      "enabled": true
    }'
  ```

  ```typescript TypeScript SDK theme={null}
  await hubify.webhooks.update({
    webhookId: "w78k9l0m1n2o3p4q",
    events: ["experiment.completed", "experiment.failed", "agent.error"],
    enabled: true,
  });
  ```
</CodeGroup>

## Delete a Webhook

<CodeGroup>
  ```bash curl theme={null}
  curl -X DELETE https://www.hubify.com/api/v1/webhooks/w78k9l0m1n2o3p4q \
    -H "Authorization: Bearer $HUBIFY_TOKEN"
  ```

  ```typescript TypeScript SDK theme={null}
  await hubify.webhooks.delete({ webhookId: "w78k9l0m1n2o3p4q" });
  ```
</CodeGroup>

## Test a Webhook

Send a test payload to verify your endpoint is working correctly.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://www.hubify.com/api/v1/webhooks/w78k9l0m1n2o3p4q/test \
    -H "Authorization: Bearer $HUBIFY_TOKEN"
  ```

  ```typescript TypeScript SDK theme={null}
  const result = await hubify.webhooks.test({
    webhookId: "w78k9l0m1n2o3p4q",
  });
  // { success: true, statusCode: 200, responseTime: 142 }
  ```
</CodeGroup>

## Delivery History

View recent webhook delivery attempts and their outcomes.

<CodeGroup>
  ```bash curl theme={null}
  curl "https://www.hubify.com/api/v1/webhooks/w78k9l0m1n2o3p4q/deliveries?limit=20" \
    -H "Authorization: Bearer $HUBIFY_TOKEN"
  ```

  ```typescript TypeScript SDK theme={null}
  const deliveries = await hubify.webhooks.listDeliveries({
    webhookId: "w78k9l0m1n2o3p4q",
    limit: 20,
  });
  ```
</CodeGroup>

<ResponseField name="data" type="object[]">
  <Expandable title="Delivery object">
    <ResponseField name="id" type="string" required>Delivery ID.</ResponseField>
    <ResponseField name="eventId" type="string" required>Event ID that triggered this delivery.</ResponseField>
    <ResponseField name="eventType" type="string" required>Event type.</ResponseField>
    <ResponseField name="statusCode" type="number">HTTP response status code.</ResponseField>
    <ResponseField name="success" type="boolean" required>Whether the delivery succeeded.</ResponseField>
    <ResponseField name="attempt" type="number" required>Attempt number (1-6).</ResponseField>
    <ResponseField name="responseTime" type="number">Response time in milliseconds.</ResponseField>
    <ResponseField name="error" type="string">Error message if the delivery failed.</ResponseField>
    <ResponseField name="createdAt" type="number" required>Delivery timestamp (ms).</ResponseField>
  </Expandable>
</ResponseField>
