Why local testing matters
Three classes of bug are nearly impossible to find without local testing:- Signature verification mismatches. Code that looks correct against a unit test fixture may fail against the actual bytes Virtuous sends. The only way to catch this is with a real delivery.
- Edge-case payload shapes. Production payloads contain combinations of optional fields, custom field values, and lifecycle states that test fixtures rarely cover. Real deliveries expose these.
- Timing assumptions. Code that works at unit-test speed may not work at the latency of a real delivery — especially if it makes downstream API calls inside the handler.
The local testing setup
Three components:- A tunneling tool that exposes your localhost to the public internet as an HTTPS URL.
- A Seeded Sandbox subscription pointed at the tunnel’s URL.
- Your local webhook handler running on a local port.
Step 1: pick a tunneling tool
Three options that work well for Virtuous webhook development:| Tool | Strengths | Considerations |
|---|---|---|
| ngrok | Mature, well-documented, generous free tier, built-in request inspector. The default choice for most webhook development. | Free-tier URLs are randomized on each restart — re-subscribe the webhook every time. |
| Cloudflare Tunnel | Free, stable URLs that persist across restarts, can be tied to a domain you control. | More setup; requires a Cloudflare account and DNS configuration. |
| localtunnel | Open-source, simple. | Less reliable than ngrok or Cloudflare; not recommended for serious work. |
Setting up ngrok
https://abc123.ngrok-free.app). This is the URL you’ll register as your webhook payload URL.
The ngrok inspector is at http://localhost:4040 — a local web UI showing every request that came through the tunnel. You can view payloads, replay requests, and inspect headers. This is the most useful tool in your local-testing kit.
Step 2: subscribe to webhooks against the tunnel
Create a webhook subscription pointed at your tunnel URL. Always use a Seeded Sandbox, not a production customer organization:cURL
id — you’ll use it to delete the subscription when you’re done testing.
Step 3: run your handler locally
Start your webhook handler on the local port matching the tunnel:JavaScript
Step 4: trigger events
With everything connected, trigger events in the Seeded Sandbox and watch them arrive locally. Two ways:Via the Virtuous UI
Open the Seeded Sandbox in the Virtuous web app and perform actions that fire the events you subscribed to: create a Contact, record a Gift, archive a Project. Each action triggers the corresponding webhook delivery to your tunnel.Via the API
Sometimes faster — call the API directly to create or update records:cURL
Contact and Gift Transactions process in the nightly batch — the webhook for the resulting Contact/Gift creation does not fire immediately. For faster feedback during local development, use the direct endpoints (
POST /api/Contact, POST /api/Gift) to trigger contactCreate and giftCreate events synchronously, or edit existing records to trigger contactUpdate and giftUpdate events.Capturing fixtures for replay testing
The ngrok inspector lets you view every delivery, including the raw body and all headers. Once a real delivery has come through, save it as a fixture:JavaScript
JavaScript
Replaying deliveries
The ngrok inspector has a “Replay” button next to every captured request. Click it to re-send the same delivery to your local handler. Use this to test:- Idempotency. Replay the same delivery twice and confirm your handler produces the same observable state (and only one set of side effects).
- Code changes. Stop the local server, change your handler code, restart, then replay the captured delivery against the new code. No need to re-trigger the event in Virtuous.
- Failure paths. Configure your local server to return
500on the replay, then watch what your handler does on the inevitable retry from Virtuous (this is the cleanest way to observe retry behavior on a live subscription).
Cleaning up
When you’re done testing, delete or deactivate the webhook subscription so the tunnel URL stops receiving deliveries:cURL
CI/CD considerations
Production deployments need integration tests that don’t depend on a developer’s tunnel. Two patterns:Fixture-based CI tests
Run your fixture replay tests in CI. They validate signature verification and handler logic against captured-real-world payloads without requiring network access to Virtuous.Staging environment with persistent tunnel
For end-to-end tests, run a staging environment that maintains a persistent tunnel URL (Cloudflare Tunnel works well for this) and a permanently-subscribed Seeded Sandbox webhook. The CI suite triggers an action in the sandbox and asserts the webhook arrived at the staging endpoint. This is slower and more expensive than fixture replay but catches issues across the full delivery path. Most partner integrations only need the fixture-based approach. The full staging environment is worth setting up for high-risk integrations (financial reconciliation, donor communication) where end-to-end confidence matters.Where to go next
Webhooks Overview
Subscription management and the receiver pattern — the production-side of what you’re testing locally.
Signature Verification
The verifier code you’ll exercise most heavily during local testing.
Idempotency and Safe Reprocessing
Use ngrok’s replay button to exercise your idempotency logic with real captured deliveries.
Base URLs and Environments
Set up a Seeded Sandbox if you don’t already have one.