# Webhooks

### Overview

Webhooks allow you to receive real-time notifications when events occur in BoundaryAI. Instead of polling our API, your server receives HTTP POST requests automatically.

**Use cases:**

* Sync pushed content to your data warehouse
* Trigger workflows when surveys are created/published
* Build real-time dashboards
* Integrate with Slack, Teams, or other tools

### Prerequisites

1. **Admin access** - Only organization admins can manage webhooks
2. **HTTPS endpoint** - Your webhook URL must use HTTPS (HTTP not allowed)
3. **Public URL** - The URL must be publicly accessible (no localhost, private IPs, or internal domains)
4. **Response time** - Your endpoint should respond within 10 seconds

### Available Events

| Event              | Description                      | When it fires                                                                |
| ------------------ | -------------------------------- | ---------------------------------------------------------------------------- |
| `content.pushed`   | Content was pushed via API       | After successful `/api/input/content/push` or `/api/input/content/push/bulk` |
| `survey.created`   | A new survey was created         | After successful `/api/input/survey/create`                                  |
| `survey.published` | A survey was published           | After successful `/api/input/survey/publish`                                 |
| `series.created`   | A new feedback group was created | After successful `/api/input/survey_series/create`                           |

### Webhook Payload

```json
{
  "event": "content.pushed",
  "timestamp": "2025-01-15T10:30:00.000000Z",
  "data": {
    "survey_series_id": 123,
    "survey_id": 456,
    "question_id": 789,
    "lines_pushed": 50,
    "api_key_id": 1
  }
}
```

### HTTP Headers

Every webhook request includes these headers:

| Header                 | Description                                | Example                  |
| ---------------------- | ------------------------------------------ | ------------------------ |
| `X-Boundary-Signature` | HMAC-SHA256 signature for verification     | `sha256=abc123...`       |
| `X-Boundary-Event`     | The event type that triggered this webhook | `content.pushed`         |
| `Content-Type`         | Always JSON                                | `application/json`       |
| `User-Agent`           | Identifies BoundaryAI as the sender        | `BoundaryAI-Webhook/1.0` |

### Managing Webhooks

All webhook management endpoints require JWT authentication (not API key).

#### List Webhooks

```bash
GET /api/webhooks
Authorization: Bearer {JWT_TOKEN}
```

### <sup>Response</sup>

```
{
  "is_admin": true,
  "webhooks": [
    {
      "id": 1,
      "name": "My Webhook",
      "url": "https://example.com/webhook",
      "events": ["content.pushed", "survey.created"],
      "is_active": true,
      "failure_count": 0,
      "created_at": "2025-01-15T10:00:00+00:00",
      "last_triggered_at": null
    }
  ],
  "available_events": ["content.pushed", "survey.created", "survey.published", "series.created"]
}

```

### Create Webhook

```
POST /api/webhooks
Authorization: Bearer {JWT_TOKEN}
Content-Type: application/json

{
  "name": "My Webhook",
  "url": "https://example.com/webhook",
  "events": ["content.pushed", "survey.created"]
}
```

### <sup>Response</sup>

```
{
  "webhook": {
    "id": 1,
    "name": "My Webhook",
    "url": "https://example.com/webhook",
    "events": ["content.pushed", "survey.created"],
    "is_active": true,
    "failure_count": 0,
    "created_at": "2025-01-15T10:00:00+00:00",
    "last_triggered_at": null
  },
  "secret": "ngQCfdLY0Ykp0_OLrXlMhzPfYNo0WV1w9j4Imt1ddMM",
  "message": "Save the secret now! You will not be able to see it again."
}

⚠️ Important: Save the secret immediately - it's only shown once!
```

### Update Webhook

```
PATCH /api/webhooks/{id}
Authorization: Bearer {JWT_TOKEN}
Content-Type: application/json

{
  "name": "Updated Name",
  "events": ["content.pushed"],
  "is_active": true
}
```

### <sup>Response</sup>

```
{
  "webhook": {
    "id": 1,
    "name": "Updated Name",
    "url": "https://example.com/webhook",
    "events": ["content.pushed"],
    "is_active": true,
    "failure_count": 0,
    "created_at": "2025-01-15T10:00:00+00:00",
    "last_triggered_at": null
  }
}
```

### Single Webhook

```
GET /api/webhooks/{id}
Authorization: Bearer {JWT_TOKEN}
```

### <sup>Response</sup>

```
{
  "webhook": {
    "id": 1,
    "name": "My Webhook",
    "url": "https://example.com/webhook",
    "events": ["content.pushed"],
    "is_active": true,
    "failure_count": 0,
    "created_at": "2025-01-15T10:00:00+00:00",
    "last_triggered_at": "2025-01-15T12:00:00+00:00"
  }
}
```

### Delete Webhook

```
DELETE /api/webhooks/{id}
Authorization: Bearer {JWT_TOKEN}
```

### <sup>Response</sup>

```
{
  "message": "Webhook deleted successfully",
  "webhook_id": 1
}
```

### Verifying Signatures

**Always verify** the signature to confirm the webhook is from BoundaryAI:

#### Python

```python
import hmac
import hashlib

def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", signature)

# Usage in Flask:
@app.route('/webhook', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Boundary-Signature')
    if not verify_webhook(request.data, signature, WEBHOOK_SECRET):
        return 'Invalid signature', 401

    event = request.headers.get('X-Boundary-Event')
    data = request.json
    # Process the webhook...
    return 'OK', 200
```

#### Node.js

```javascript
const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return signature === `sha256=${expected}`;
}

// Usage in Express:
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
  const signature = req.headers['x-boundary-signature'];
  if (!verifyWebhook(req.body, signature, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const event = req.headers['x-boundary-event'];
  const data = JSON.parse(req.body);
  // Process the webhook...
  res.status(200).send('OK');
});
```

### Retry Policy

If your endpoint fails, we retry with exponential backoff:

| Attempt   | Delay      |
| --------- | ---------- |
| 1st retry | 1 minute   |
| 2nd retry | 5 minutes  |
| 3rd retry | 15 minutes |

After **10 consecutive failures**, the webhook is automatically disabled.

***

## Error Reference

### Error Response Format

```json
{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable description",
    "details": { }
  }
}
```

### Error Codes

#### Authentication Errors (4xx)

| Code                  | HTTP | Description                       | Solution                          |
| --------------------- | ---- | --------------------------------- | --------------------------------- |
| `MISSING_AUTH`        | 401  | No Authorization header           | Add `Authorization: Bearer {key}` |
| `INVALID_AUTH_FORMAT` | 401  | Malformed header                  | Use `Bearer {key}` format         |
| `INVALID_TOKEN`       | 401  | Key doesn't exist or wrong format | Check key, create new if needed   |
| `REVOKED_KEY`         | 401  | Key was revoked                   | Create a new API key              |

#### Permission Errors (4xx)

| Code                      | HTTP | Description                     | Solution                        |
| ------------------------- | ---- | ------------------------------- | ------------------------------- |
| `INSUFFICIENT_PERMISSION` | 403  | Key lacks required permission   | Use key with correct permission |
| `FORBIDDEN`               | 403  | Resource belongs to another org | Check IDs are correct           |

#### Resource Errors (4xx)

| Code                      | HTTP | Description                  | Solution                  |
| ------------------------- | ---- | ---------------------------- | ------------------------- |
| `SURVEY_SERIES_NOT_FOUND` | 404  | Feedback group doesn't exist | Verify `survey_series_id` |
| `SURVEY_NOT_FOUND`        | 404  | Data source doesn't exist    | Verify `survey_id`        |
| `QUESTION_NOT_FOUND`      | 404  | Question doesn't exist       | Verify `question_id`      |

#### Validation Errors (4xx)

| Code                    | HTTP | Description                       | Solution                               |
| ----------------------- | ---- | --------------------------------- | -------------------------------------- |
| `VALIDATION_ERROR`      | 400  | Invalid request data              | Check `details` for specific fields    |
| `INVALID_QUESTION_TYPE` | 400  | Pushing text to non-text question | Use question with `accepts_text: true` |

#### Rate & Quota Errors (4xx)

| Code               | HTTP | Description                | Solution                            |
| ------------------ | ---- | -------------------------- | ----------------------------------- |
| `RATE_LIMITED`     | 429  | Too many requests          | Wait for `Retry-After` seconds      |
| `INSUFFICIENT_APS` | 402  | Not enough analysis points | Purchase more APS or use `test` key |


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://boundaryai.gitbook.io/boundaryai-docs/api-and-webhooks/webhooks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
