SearchJetEngine

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