Webhooks
## Overview
Webhooks allow SearchJet to send real-time HTTP notifications to your server when specific events occur. Instead of polling for changes, your application receives instant updates about document indexing, quota thresholds, and API key activity.
---
## Create a Webhook
Register a new webhook endpoint to receive event notifications.
### Endpoint
```
POST /api/v1/sites/{siteId}/webhooks
```
### Request Body
```json
{
"url": "https://your-server.com/webhooks/searchjet",
"events": ["document.indexed", "search.quota_exceeded"],
"secret": "whsec_your_webhook_secret_here",
"active": true,
"description": "Production webhook for indexing notifications"
}
```
| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string | Yes | HTTPS endpoint to receive webhook payloads |
| `events` | array | Yes | Array of event types to subscribe to |
| `secret` | string | No | Shared secret for signature verification |
| `active` | boolean | No | Enable/disable the webhook (default: true) |
| `description` | string | No | Human-readable label |
### Response
```json
{
"id": "wh_abc123",
"siteId": "site_abc123",
"url": "https://your-server.com/webhooks/searchjet",
"events": ["document.indexed", "search.quota_exceeded"],
"secret": "whsec_your_webhook_secret_here",
"active": true,
"createdAt": "2025-07-10T14:00:00Z"
}
```
---
## List Webhooks
Retrieve all webhooks configured for your site.
### Endpoint
```
GET /api/v1/sites/{siteId}/webhooks
```
### Response
```json
{
"webhooks": [
{
"id": "wh_abc123",
"url": "https://your-server.com/webhooks/searchjet",
"events": ["document.indexed"],
"active": true,
"createdAt": "2025-07-10T14:00:00Z"
},
{
"id": "wh_def456",
"url": "https://staging-server.com/webhooks/searchjet",
"events": ["document.indexed", "document.deleted"],
"active": false,
"createdAt": "2025-07-05T09:00:00Z"
}
],
"total": 2
}
```
---
## Delete a Webhook
Remove a webhook permanently. This action cannot be undone.
### Endpoint
```
DELETE /api/v1/sites/{siteId}/webhooks/{webhookId}
```
### Response
```json
{
"deleted": true,
"id": "wh_abc123"
}
```
---
## Available Events
| Event | Description |
|---|---|
| `document.indexed` | A document was successfully added or updated in the index |
| `document.deleted` | A document was removed from the index |
| `site.created` | A new site was created in your account |
| `search.quota_exceeded` | A search quota threshold (75%, 90%, 100%) was reached |
| `key.created` | A new API key was generated |
| `key.revoked` | An API key was deleted or revoked |
Subscribe to only the events you need. Unnecessary events increase load on your server.
---
## Webhook Payload Format
All webhook payloads follow a consistent structure:
```json
{
"id": "evt_xyz789",
"type": "document.indexed",
"created_at": "2025-07-10T14:30:00Z",
"site_id": "site_abc123",
"data": {
"document_id": "doc_001",
"title": "Getting Started with SearchJet",
"indexed_at": "2025-07-10T14:30:00Z",
"field_count": 8
}
}
```
### Event-Specific Data
#### document.indexed
```json
{
"document_id": "doc_001",
"title": "Getting Started with SearchJet",
"indexed_at": "2025-07-10T14:30:00Z",
"field_count": 8
}
```
#### document.deleted
```json
{
"document_id": "doc_001",
"deleted_at": "2025-07-10T14:35:00Z"
}
```
#### search.quota_exceeded
```json
{
"quota_type": "searches",
"threshold": 90,
"used": 450000,
"limit": 500000,
"plan": "pro"
}
```
#### key.created / key.revoked
```json
{
"key_id": "key_abc123",
"key_prefix": "sj_live_...",
"name": "Production Key",
"created_at": "2025-07-10T14:00:00Z"
}
```
---
## Signature Verification
Every webhook request includes an `X-SearchJet-Signature` header for verification. The signature is an HMAC-SHA256 hash of the request body using your webhook secret.
### Header
```
X-SearchJet-Signature: sha256=abc123def456...
```
### Verification
#### Node.js / Express
```javascript
const crypto = require("crypto");
function verifyWebhookSignature(payload, signature, secret) {
const expected = "sha256=" +
crypto.createHmac("sha256", secret)
.update(payload, "utf8")
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// In your Express route:
app.post("/webhooks/searchjet", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-searchjet-signature"];
const isValid = verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(req.body);
console.log(`Received event: ${event.type}`);
res.status(200).json({ received: true });
});
```
#### Python / Flask
```python
import hmac
import hashlib
def verify_signature(payload_body, signature, secret):
expected = "sha256=" + hmac.new(
secret.encode("utf-8"),
payload_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route("/webhooks/searchjet", methods=["POST"])
def webhook():
signature = request.headers.get("X-SearchJet-Signature", "")
if not verify_signature(request.data, signature, WEBHOOK_SECRET):
return jsonify({"error": "Invalid signature"}), 401
event = request.get_json()
print(f"Received event: {event['type']}")
return jsonify({"received": True}), 200
```
#### PHP
```php
function verifySignature(string $payload, string $signature, string $secret): bool {
$expected = "sha256=" . hash_hmac("sha256", $payload, $secret);
return hash_equals($expected, $signature);
}
$payload = file_get_contents("php://input");
$signature = $_SERVER["HTTP_X_SEARCHJET_SIGNATURE"] ?? "";
if (!verifySignature($payload, $signature, $_ENV["WEBHOOK_SECRET"])) {
http_response_code(401);
echo json_encode(["error" => "Invalid signature"]);
exit;
}
$event = json_decode($payload, true);
error_log("Received event: {$event['type']}");
http_response_code(200);
echo json_encode(["received" => true]);
```
---
## Retry Policy
SearchJet retries failed webhook deliveries with the following policy:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 8 hours |
| 7 | 24 hours |
After 7 failed attempts, the webhook is marked as inactive and you receive an email notification.
A webhook is considered failed if:
- The endpoint returns a non-2xx status code
- The endpoint does not respond within 30 seconds
- The endpoint is unreachable
---
## Testing Webhooks
### cURL — Send Test Event
```bash
curl -X POST "https://app.searchjetengine.com/api/v1/sites/site_abc123/webhooks/test" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"webhook_id": "wh_abc123", "event": "document.indexed"}'
```
### Local Testing with ngrok
For local development, use ngrok to expose your local server:
```bash
ngrok http 3000
```
Then configure your webhook URL to the ngrok HTTPS URL:
```json
{
"url": "https://abc123.ngrok.io/webhooks/searchjet",
"events": ["document.indexed"],
"secret": "whsec_test_secret"
}
```
---
## Best Practices
- **Always verify signatures.** Never process webhook payloads without verifying the `X-SearchJet-Signature` header.
- **Respond quickly.** Return a 2xx status code within 5 seconds. Process heavy workloads asynchronously.
- **Use HTTPS only.** Webhook URLs must use HTTPS. HTTP endpoints are rejected.
- **Idempotent processing.** Process the same event multiple times without side effects. Use the `id` field for deduplication.
- **Log everything.** Store webhook events for debugging and audit purposes.
- **Monitor webhook health.** Check the webhook list endpoint for inactive webhooks and re-enable them.
---
## Related Pages
- [Index Management API](/docs/index-api) — Trigger indexing that fires webhooks
- [Rate Limits](/docs/rate-limits) — Understand quota thresholds in webhooks
- [Error Handling](/docs/error-handling) — Handle API errors gracefully