Skip to content

Testing Webhooks Locally

This page walks through the loop of building a webhook receiver, debugging a failed delivery, and replaying it once you’ve shipped a fix.

BuildWorkPro only delivers to public HTTPS URLs (and the URL is SSRF-validated, so loopback and private ranges are rejected). To receive deliveries on your laptop, expose your dev server through a tunnel.

  1. Start your local receiver.

    Terminal window
    node receiver.js # listening on localhost:3000
  2. Open a tunnel. Pick whichever tool you already use:

    Terminal window
    # ngrok
    ngrok http 3000
    # localtunnel
    npx localtunnel --port 3000
    # Cloudflare Tunnel
    cloudflared tunnel --url http://localhost:3000

    Note the public HTTPS URL the tunnel prints.

  3. Register the endpoint.

    Terminal window
    curl -X POST https://app.buildworkpro.com/api/v1/webhook-endpoints \
    -H "Authorization: Bearer ${BUILDWORKPRO_API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{
    "url": "https://abcd-1234.ngrok-free.app/bwp",
    "eventTypes": ["bid.accepted", "project.status_changed"]
    }'
  4. Trigger an event. Accept a bid in the dashboard, or call POST /api/v1/bids/{id}/accept from your terminal. The delivery should arrive within a second or two.

Open Settings -> Developer -> Webhooks in the app and click into an endpoint to see its delivery history. Each row shows:

  • Event type and event id — click the row to expand the full payload.
  • Statuspending, succeeded, failed, or exhausted.
  • Attempt count — how many times BuildWorkPro has tried this delivery.
  • Response status — the last HTTP code your receiver returned.
  • Response body — captured up to 10 KB.
  • Response headers — captured: content-type, x-request-id, x-trace-id.
  • Error message — if the attempt failed before getting a response (network error, SSRF check, etc.).
  • Next attempt at — when BuildWorkPro will try again, if applicable.

The same data is available programmatically:

Terminal window
# List recent deliveries for an endpoint
curl https://app.buildworkpro.com/api/v1/webhook-endpoints/17/deliveries \
-H "Authorization: Bearer ${BUILDWORKPRO_API_KEY}"
# Get one delivery in full
curl https://app.buildworkpro.com/api/v1/webhook-deliveries/9001 \
-H "Authorization: Bearer ${BUILDWORKPRO_API_KEY}"

After fixing a bug in your receiver, replay any delivery that failed before the fix shipped:

Terminal window
curl -X POST https://app.buildworkpro.com/api/v1/webhook-deliveries/9001/replay \
-H "Authorization: Bearer ${BUILDWORKPRO_API_KEY}"

Replay resets attemptCount to zero, sets status back to pending, clears the response/error fields, and re-enqueues the delivery for an immediate attempt. The eventId and signed payload bytes are unchanged, so signature verification on your end still works.

You can also click Replay on any delivery row in the dashboard.

Failed deliveries are retried up to seven times on this schedule, with ±20% jitter applied to spread thundering-herd load:

AttemptDelay from previousCumulative
10s (immediate)0s
230s30s
32 min~2.5 min
415 min~17.5 min
51 hour~1.3 hours
66 hours~7.3 hours
724 hours~31 hours

After the seventh attempt, the delivery is marked exhausted and never retried automatically — replay it manually once you’ve fixed the receiver.

Receiver returnsWhat BuildWorkPro does
200-299Mark succeeded. Reset the endpoint’s consecutive-failure counter.
408 Request TimeoutRetriable. Schedule next attempt per the table above.
429 Too Many RequestsRetriable. Schedule next attempt per the table above.
Other 4xx (400, 401, 404, 422, …)Permanent failure. Stop retrying immediately, mark exhausted.
Any 5xxRetriable. Schedule next attempt per the table above.
Network error, TLS failure, or 10s timeoutRetriable. Same schedule as 5xx.

Endpoints that fail 20 deliveries in a row are automatically disabled (isActive: false, disabledReason populated). This protects your receiver and ours from a runaway loop when an endpoint goes permanently dark (subdomain expired, receiver decommissioned, repeated 4xx).

A successful delivery at any point resets the counter to zero. Re-enable a disabled endpoint via PATCH /api/v1/webhook-endpoints/{id} with { "isActive": true } once you’ve confirmed the receiver is healthy.

  • Body parsing. If you mount express.json() before your webhook route, req.body becomes an object and the HMAC won’t match. Use express.raw({ type: "application/json" }) on the webhook path.
  • Clock skew. Signature verification rejects timestamps more than 5 minutes from the receiver’s clock. Run NTP.
  • HTTPS only. BuildWorkPro will not deliver to http://. Tunnels like ngrok and Cloudflare Tunnel give you HTTPS for free.
  • Private IPs. Even via a tunnel, the resolved IP must be public. The SSRF guard runs at registration and at every delivery attempt as a defense against DNS rebinding.
  • Slow responses. The request times out after 10 seconds. Persist the event id and return 2xx immediately; do real work asynchronously.