Documentation Index
Fetch the complete documentation index at: https://brezelscraper.com/docs/llms.txt
Use this file to discover all available pages before exploring further.
Webhooks let BrezelScraper send an HTTP POST to your server when a job completes, fails, or is cancelled. You register a URL, and BrezelScraper calls it with a small JSON payload every time a job reaches a final state.
Quick start
- Go to Dashboard > Integrations and create a webhook.
- Paste an HTTPS endpoint URL from your server or automation tool.
- Copy the signing secret and store it securely. You will only see it once.
- Create a scraping job. When the job finishes, BrezelScraper sends a POST to your URL.
- Your endpoint receives the event, verifies the signature, and fetches results through the API.
Event types
BrezelScraper sends one of three event types:
| Event type | When it fires |
|---|
job.completed | The job finished successfully. Results are ready. |
job.failed | The job could not complete. Check failure_reason on the job. |
job.cancelled | The job was cancelled by the user or by a timeout. |
Every webhook delivery is an HTTP POST with a JSON body:
{
"event_type": "job.completed",
"job_id": "d8d8a24e-cef2-4b02-8396-abc290b6f299",
"job_name": "Berlin cafes",
"status": "completed",
"result_count": 87,
"created_at": "2026-04-10T12:00:00Z",
"ended_at": "2026-04-10T12:05:30Z"
}
| Field | Type | Description |
|---|
event_type | string | One of job.completed, job.failed, job.cancelled |
job_id | string | The job’s UUID. Use this to fetch results from the API. |
job_name | string | The name you gave the job when you created it. |
status | string | The job’s final status. |
result_count | integer | Number of places scraped. 0 if the job failed or found nothing. |
created_at | string | When the job was created (ISO 8601). |
ended_at | string | When the job reached its final status (ISO 8601). |
The payload contains metadata only. To get the scraped data, call GET /api/v1/jobs/{job_id}/results after receiving the webhook.
Each delivery includes these headers:
| Header | Example | Purpose |
|---|
Content-Type | application/json | Always JSON. |
User-Agent | BrezelScraper-Webhook/1.0 | Identifies the sender. |
X-Webhook-Signature | t=1712956800,sha256=a1b2c3... | Signed timestamp and HMAC-SHA256 of the request body. |
X-Webhook-ID | 019d837e-8e1c-... | Unique ID for this delivery. Use it to deduplicate. |
The signature header contains two parts separated by a comma:
t=<unix_seconds> is the timestamp when the delivery was sent
sha256=<hex> is the HMAC-SHA256 of <timestamp>.<body> using your signing secret
The timestamp is included in the signed payload, so it cannot be forged separately.
Verify the signature
Parse the X-Webhook-Signature header, extract the timestamp and signature hash, then compute the expected HMAC over timestamp + "." + body.
Node.js
const crypto = require('crypto');
function verifyWebhook(body, secret, signatureHeader) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(p => p.split('='))
);
const timestamp = parts.t;
const signature = parts.sha256;
// Reject deliveries older than 5 minutes
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + body, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// In your handler:
const isValid = verifyWebhook(rawBody, process.env.WEBHOOK_SECRET, req.headers['x-webhook-signature']);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
Python
import hmac
import hashlib
import time
def verify_webhook(body: bytes, secret: str, signature_header: str) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
timestamp = parts["t"]
signature = parts["sha256"]
# Reject deliveries older than 5 minutes
if int(time.time()) - int(timestamp) > 300:
return False
expected = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"strings"
)
func verifyWebhook(body []byte, secret string, signatureHeader string) bool {
parts := make(map[string]string)
for _, p := range strings.Split(signatureHeader, ",") {
kv := strings.SplitN(p, "=", 2)
if len(kv) == 2 {
parts[kv[0]] = kv[1]
}
}
timestamp := parts["t"]
signature := parts["sha256"]
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(timestamp))
mac.Write([]byte("."))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return subtle.ConstantTimeCompare([]byte(signature), []byte(expected)) == 1
}
Always use constant-time comparison (timingSafeEqual in Node.js, compare_digest in Python, subtle.ConstantTimeCompare in Go) instead of === or ==. Regular string comparison can leak timing information that helps attackers forge signatures.
The timestamp is cryptographically bound to the signature. Reject deliveries where t is more than 5 minutes old to prevent replay attacks.
Retries
If your endpoint returns a non-2xx status code or does not respond, BrezelScraper retries the delivery up to 5 times with increasing delays:
| Attempt | Delay |
|---|
| 1 | Immediate |
| 2 | ~2 seconds |
| 3 | ~4 seconds |
| 4 | ~8 seconds |
| 5 | ~16 seconds |
After 5 failed attempts, the delivery is marked as failed and not retried again.
Each retry uses the same X-Webhook-ID, so you can deduplicate on that header if your endpoint received the same delivery more than once.
Rate limits
To protect your endpoint and prevent abuse, BrezelScraper limits webhook deliveries to:
- 100 deliveries per hour per user account
- 50 deliveries per hour per destination IP address
If a delivery is rate-limited, it is automatically retried after the limit window resets.
Requirements and limits
| Setting | Value |
|---|
| Protocol | HTTPS only |
| Max active webhooks per account | 10 |
| Webhook name length | 1 to 100 characters |
| URL length | Up to 2,048 characters |
| Response timeout | 10 seconds |
| Max retry attempts | 5 |
| Signing algorithm | HMAC-SHA256 |
URLs pointing to private networks, localhost, or cloud metadata endpoints are rejected.
Manage webhook configurations
POST /api/v1/webhooks
Create a new webhook.
Request body:
{
"name": "My webhook",
"url": "https://example.com/webhooks/brezel"
}
Response (201 Created):
{
"id": "018efdf7-7fe1-7c42-b4e2-123456789abc",
"name": "My webhook",
"url": "https://example.com/webhooks/brezel",
"secret": "fdc7992336902be147a38f36a24876828f4207d5820d430140c9602c04fbd7a5"
}
The secret is your signing key. Store it securely. It is shown only at creation time.
GET /api/v1/webhooks
List all webhook configurations for your account, including revoked ones.
Response (200 OK):
[
{
"id": "018efdf7-7fe1-7c42-b4e2-123456789abc",
"name": "My webhook",
"url": "https://example.com/webhooks/brezel",
"verified_at": "2026-04-10T09:00:00Z",
"created_at": "2026-04-10T09:00:00Z",
"updated_at": "2026-04-10T09:00:00Z"
}
]
verified_at is set after the first successful delivery to this webhook.
This endpoint returns a plain array because each account is limited to 10 webhooks. No pagination is needed.
PATCH /api/v1/webhooks/{id}
Update a webhook’s name or URL. Both fields are optional.
{
"name": "Renamed webhook"
}
Returns 204 No Content on success.
DELETE /api/v1/webhooks/{id}
Revoke a webhook. Revoked webhooks stop receiving deliveries.
Returns 204 No Content on success.
Best practices
- Respond quickly. Return a 200 status code as fast as possible. If you need to do heavy processing, queue the work and respond immediately.
- Verify every delivery. Always check the
X-Webhook-Signature header before acting on the payload.
- Deduplicate. Use
X-Webhook-ID to detect and skip duplicate deliveries.
- Fetch results from the API. The webhook payload tells you a job finished. Call
GET /api/v1/jobs/{job_id}/results to get the actual data.
- Handle all event types. Your endpoint should return 200 for events it does not care about. Returning an error causes unnecessary retries.