DocsOpen app
Docs/Reference/Webhooks

Webhooks: Receive Scored Opportunities Anywhere

Prowlo can fire HMAC-signed JSON payloads at any URL whenever a new batch of scored Reddit opportunities is ready. Pipe them into Slack via an incoming hook, into Zapier or Make for downstream automation, into your CRM, or into a homegrown service.

What you get

  • HMAC-SHA256 signed payloads. Every request includes an X-Prowlo-Signature header you verify against your per-webhook signing secret.
  • Automatic retries with exponential backoff. Non-2xx responses re-queue. The delivery log shows every attempt.
  • Test endpoint + manual replay. Fire a sample payload immediately on creation; replay any failed delivery from the dashboard.
  • Auto-disable on consecutive failures. If your endpoint stays broken, the webhook auto-disables with a clear reason instead of silently dropping signal.
  • Available on every plan. Webhooks are not gated to enterprise. Same primitives on the $23/mo Starter as on Growth.

Create a webhook

1

Open the Webhooks page

In your Prowlo dashboard, go to Notifications → Webhooks. Click Add webhook.

2

Configure the endpoint

Fill in three fields:

  • URL — your endpoint that will receive POST requests. Must be HTTPS in production.
  • Description — optional, for your own bookkeeping (e.g. "Slack #leads", "Zapier sheet sync").
  • Events — pick which events to subscribe to: crawl.run.completed (fires once per crawl run, including zero-result and errored runs) and alert.fired. More events on the roadmap.
3

Save the signing secret

On creation, Prowlo returns a one-time signing secret. Store it in your secrets manager — you will use it to verify incoming requests. The secret is shown only once; if you lose it, rotate it from the same page (one click).

4

Click Test

Use the Test button to fire a sample payload at your URL. The delivery log will show whether your endpoint accepted it and what status code it returned.

Payload structure

Every payload is JSON with this shape:

{
  "type": "crawl.run.completed",
  "created_at": "2026-04-29T19:12:33.421Z",
  "id": "d_01H8XYZ...",
  "data": {
    "organization_id": "org_01H...",
    "run_id": "run_01H...",
    "watcher_id": "wch_01H...",
    "watcher_name": "SaaS founders",
    "platform": "reddit",
    "subreddit": "SaaS",
    "status": "ok",
    "posts_fetched": 100,
    "matched": 12,
    "tagged": 8,
    "new_posts": 5,
    "errored": false,
    "error": null,
    "duration_ms": 842,
    "started_at": "2026-04-29T19:12:32.579Z"
  }
}

The event name is in the top-level type field (also sent as the Prowlo-Event header). The exact shape of data depends on the event type — always check type in your handler before parsing data. On an errored run, status is "error", errored is true, and error carries the message.

Verify the HMAC signature

Prowlo signs every payload with HMAC-SHA256 using your per-webhook signing secret. The signed string is the Prowlo-Timestamp header value, a literal ., then the exact raw request body i.e. {timestamp}.{body}. The hex digest ships in the Prowlo-Signature header as v1=<hex>; strip the v1= prefix before comparing.

Always verify in constant time before processing the payload, and reject requests whose Prowlo-Timestamp is too far from now (e.g. > 5 minutes) to blunt replay attacks.

Node.js (Express)

import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.PROWLO_WEBHOOK_SECRET;

// IMPORTANT: capture the raw body for signature verification
app.use('/prowlo', express.raw({ type: 'application/json' }));

app.post('/prowlo', (req, res) => {
  const timestamp = req.header('Prowlo-Timestamp') ?? '';
  // Header is "v1=<hex>" — strip the scheme prefix before comparing.
  const signature = (req.header('Prowlo-Signature') ?? '').replace(/^v1=/, '');
  const rawBody = req.body.toString('utf8');   // raw string, not re-serialized JSON

  const expected = crypto
    .createHmac('sha256', SECRET)
    .update(`${timestamp}.${rawBody}`)        // signed payload: "{timestamp}.{body}"
    .digest('hex');

  const sigBuf = Buffer.from(signature, 'hex');
  const expBuf = Buffer.from(expected, 'hex');
  const ok = sigBuf.length === expBuf.length && crypto.timingSafeEqual(sigBuf, expBuf);
  if (!ok) return res.status(401).send('bad signature');

  // Reject stale timestamps (replay protection).
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return res.status(401).send('stale timestamp');
  }

  const payload = JSON.parse(rawBody);
  // ... handle payload.type, payload.data ...

  res.status(200).send('ok');
});

Python (Flask)

import hashlib
import hmac
import os
import time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ['PROWLO_WEBHOOK_SECRET'].encode('utf-8')

@app.post('/prowlo')
def prowlo_webhook():
    timestamp = request.headers.get('Prowlo-Timestamp', '')
    # Header is "v1=<hex>" — strip the scheme prefix before comparing.
    signature = request.headers.get('Prowlo-Signature', '').removeprefix('v1=')
    signed = f"{timestamp}.".encode('utf-8') + request.data   # b"{timestamp}.{body}"
    expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(signature, expected):
        abort(401)
    # Reject stale timestamps (replay protection).
    if abs(time.time() - int(timestamp)) > 300:
        abort(401)
    payload = request.get_json(force=True)
    # ... handle payload['type'], payload['data'] ...
    return ('', 200)

curl (manual test)

# Recompute the signature Prowlo sends. The signed string is "{timestamp}.{body}";
# the output matches the v1=<hex> value in the Prowlo-Signature header.
TIMESTAMP=1714417953
BODY='{"type":"crawl.run.completed","id":"evt_test"}'
printf '%s' "$TIMESTAMP.$BODY" \
  | openssl dgst -sha256 -hmac "$PROWLO_WEBHOOK_SECRET" \
  | awk '{print "v1="$2}'

Use this to verify your handler matches what Prowlo computes over the same Prowlo-Timestamp + body.

Retries, replay, and auto-disable

  • Retry policy. Any non-2xx response triggers a retry with exponential backoff. Successive failures get progressively longer delays.
  • Delivery log. Every attempt is logged with status code, response body snippet, and error message. Filter by webhook or status. Found in the dashboard at Notifications → Webhooks → Deliveries.
  • Manual replay. Click Redeliver on any failed entry to re-fire the original payload. Useful after deploying a fix to your handler.
  • Auto-disable. If your endpoint fails consecutively past the failure threshold, the webhook auto-disables with a clear reason (timeouts, 5xx, signature mismatch, etc.). Re-enable from the Webhooks page once your endpoint is fixed.
  • Idempotency. The event_id field is stable across retries. Use it as your idempotency key so a replay does not duplicate downstream side effects.

Common destinations

Slack

Easiest path: in Slack, create an incoming webhook for the channel you want opportunities to post to. Paste that URL into Prowlo as the webhook destination. Slack will format the JSON payload and post the message into the channel automatically.

Zapier / Make / n8n

All three platforms support generic webhook triggers as a first-class integration. Create a "Catch Webhook" trigger in Zapier (or equivalent in Make/n8n), copy the URL it gives you, paste it into Prowlo. Downstream you can route opportunities to a CRM, spreadsheet, or chat.

Discord

Discord channels expose a webhook URL natively (Channel Settings → Integrations → Webhooks → New Webhook). Same flow as Slack — paste the URL into Prowlo and Discord posts the JSON into the channel.

Telegram

Create a Telegram bot via BotFather, get the bot token, and use a tiny adapter (Cloudflare Worker, Vercel Edge function, or a one-line Zap) to forward Prowlo's payload to https://api.telegram.org/bot<token>/sendMessage. ~10 lines of code total.

Troubleshooting

My endpoint never receives the test payload

  1. Confirm the URL is publicly reachable (use curl -X POST from a different network).
  2. Check your firewall allows POST from Prowlo's outbound IPs.
  3. Verify your TLS certificate is valid (we don't accept self-signed in production).
  4. Check the delivery log — if attempts are showing as failed, the response status code and error message will tell you what your handler returned.

Signature verification fails locally but the payload is valid

  1. You must compute the HMAC over the raw bytes of the request body, not over a re-serialized JSON object. Re-serialization can re-order keys or add/remove whitespace, breaking the signature.
  2. In Express, that means express.raw() middleware on the route, NOT express.json().
  3. Use constant-time comparison (crypto.timingSafeEqual, hmac.compare_digest) — not ===.

My webhook auto-disabled. How do I re-enable?

Go to Notifications → Webhooks. The disabled webhook will show a reason (e.g. "5 consecutive 5xx responses"). Fix the underlying issue, then click Re-enable. Past failed deliveries can be replayed individually from the delivery log.