Webhooks
Webhooks let you receive real-time HTTP notifications when events happen on your queries. Instead of polling the API, configure a webhook URL and GetABrain.ai will push event data to your server as it occurs.
Setting Up Webhooks
There are two ways to configure webhooks:
- Per-query: Include a
webhook_urlfield when creating a query. Events for that query will be sent to the specified URL. - Account-level: Set a default webhook URL via
PUT /requestor/webhookwith{ "webhook_url": "https://..." }. The response includes your signing webhook secret (shown once). All query events go there unless overridden per-query. URLs must be publichttps://.
Example: Creating a Query with Webhook
curl -X POST https://www.getabrain.ai/api/v1/queries \
-H "Content-Type: application/json" \
-H "X-API-Key: your_api_key" \
-H "X-API-Secret: your_api_secret" \
-d '{
"type": "text",
"title": "Product Feedback",
"content_data": {
"question": "What do you think of our checkout flow?"
},
"required_responses": 3,
"bid_amount_cents": 50,
"webhook_url": "https://your-server.com/webhooks/getabrain"
}'Webhook Events
GetABrain.ai sends the following event types:
| Event | Trigger |
|---|---|
response.submitted | A worker submits a response to your query |
query.completed | All required responses have been collected and the query is marked complete |
response.submitted Payload
{
"event": "response.submitted",
"timestamp": "2025-01-15T10:35:00Z",
"data": {
"query_id": "550e8400-e29b-41d4-a716-446655440000",
"response_id": "resp-001",
"response_data": {
"answer": "The checkout flow is smooth but could use a progress indicator."
},
"quality_score": 0.91,
"received_responses": 1,
"required_responses": 3
}
}query.completed Payload
{
"event": "query.completed",
"timestamp": "2025-01-15T10:45:00Z",
"data": {
"query_id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Product Feedback",
"type": "text",
"required_responses": 3,
"received_responses": 3,
"total_cost_cents": 150,
"responses": [
{
"id": "resp-001",
"response_data": { "answer": "The checkout flow is smooth." },
"quality_score": 0.91
},
{
"id": "resp-002",
"response_data": { "answer": "Add a progress bar to the checkout." },
"quality_score": 0.88
},
{
"id": "resp-003",
"response_data": { "answer": "Consider adding guest checkout." },
"quality_score": 0.93
}
]
}
}Signature Verification
Every webhook request includes an HMAC SHA-256 signature in the X-GAB-Signature header. The signature is computed using your webhook secret (returned once by PUT /requestor/webhook) as the key and the raw request body as the message — not your API secret, which the server never stores in a signable form. Always verify this signature before processing webhooks. Deliveries time out after 10 seconds; a non-2xx answer (including redirects) is retried roughly every minute, up to 5 attempts.
Headers Sent
| Header | Description |
|---|---|
Content-Type | application/json |
X-GAB-Signature | HMAC SHA-256 hex digest of the request body |
X-GAB-Event | The event type (e.g., response.submitted) |
X-GAB-Timestamp | ISO 8601 timestamp of when the event was sent |
Node.js Verification Example
import crypto from "crypto";
function verifyWebhookSignature(body, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(body, "utf-8")
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
}
// In your webhook handler:
app.post("/webhooks/getabrain", (req, res) => {
const signature = req.headers["x-gab-signature"];
const rawBody = req.rawBody; // Ensure you capture the raw body
if (!verifyWebhookSignature(rawBody, signature, process.env.GAB_WEBHOOK_SECRET)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = req.body;
console.log("Received event:", event.event);
// Process the event...
res.status(200).json({ received: true });
});Python Verification Example
import hmac
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
def verify_signature(payload, signature, secret):
expected = hmac.new(
secret.encode("utf-8"),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route("/webhooks/getabrain", methods=["POST"])
def webhook_handler():
signature = request.headers.get("X-GAB-Signature")
if not verify_signature(request.data, signature, "your_api_secret"):
return jsonify({"error": "Invalid signature"}), 401
event = request.json
print(f"Received event: {event['event']}")
# Process the event...
return jsonify({"received": True}), 200Retry Policy
If your webhook endpoint returns a non-2xx status code (or a redirect) or does not respond within 10 seconds, GetABrain.ai will redeliver the event roughly once a minute, for up to 5 attempts total.
After 5 failed attempts, the delivery is dead-lettered and no further retries are made. You can check for missed events by polling the query endpoint.
Best Practices
- Always verify signatures -- never process a webhook without validating the
X-GAB-Signatureheader. - Respond quickly -- return a 200 status code within 10 seconds. Perform heavy processing asynchronously.
- Handle duplicates -- webhook deliveries may be retried, so use the event timestamp and query/response IDs to deduplicate.
- Use HTTPS -- webhook URLs must use HTTPS. HTTP endpoints will be rejected.
- Log everything -- store raw webhook payloads for debugging and auditing purposes.