Skip to main content

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

  1. Go to Dashboard > Integrations and create a webhook.
  2. Paste an HTTPS endpoint URL from your server or automation tool.
  3. Copy the signing secret and store it securely. You will only see it once.
  4. Create a scraping job. When the job finishes, BrezelScraper sends a POST to your URL.
  5. Your endpoint receives the event, verifies the signature, and fetches results through the API.

Event types

BrezelScraper sends one of three event types:
Event typeWhen it fires
job.completedThe job finished successfully. Results are ready.
job.failedThe job could not complete. Check failure_reason on the job.
job.cancelledThe job was cancelled by the user or by a timeout.

Payload format

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"
}
FieldTypeDescription
event_typestringOne of job.completed, job.failed, job.cancelled
job_idstringThe job’s UUID. Use this to fetch results from the API.
job_namestringThe name you gave the job when you created it.
statusstringThe job’s final status.
result_countintegerNumber of places scraped. 0 if the job failed or found nothing.
created_atstringWhen the job was created (ISO 8601).
ended_atstringWhen 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.

Headers

Each delivery includes these headers:
HeaderExamplePurpose
Content-Typeapplication/jsonAlways JSON.
User-AgentBrezelScraper-Webhook/1.0Identifies the sender.
X-Webhook-Signaturet=1712956800,sha256=a1b2c3...Signed timestamp and HMAC-SHA256 of the request body.
X-Webhook-ID019d837e-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)

Go

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:
AttemptDelay
1Immediate
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

SettingValue
ProtocolHTTPS only
Max active webhooks per account10
Webhook name length1 to 100 characters
URL lengthUp to 2,048 characters
Response timeout10 seconds
Max retry attempts5
Signing algorithmHMAC-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.