localhost, not the public internet. This page covers the patterns for getting webhook events to a local development environment, generating test events, validating signature verification, and inspecting deliveries.
The audience is partner integration developers building or debugging webhook handlers. The patterns work for any framework — the examples use Node.js, but the principles apply equally to Python, Ruby, Go, etc.
What you’ll need
| Requirement | Examples |
|---|---|
A tunneling tool that exposes localhost over HTTPS | ngrok, Cloudflare Tunnel, Tailscale Funnel, localtunnel |
| A Raise developer organization with test mode available | Coordinate with your partner manager — see Base URLs and Environments |
| A local webhook receiver | The HTTP server your integration runs |
| The test-payment-method generator | POST /api/Raise/generate-test-payment-method |
Step 1: expose your local server over HTTPS
The webhook subscription’snotificationUrl must be HTTPS — Raise won’t deliver to plain HTTP. The simplest path: a tunneling tool that proxies a public HTTPS URL to your local server.
Using ngrok
https://abc123.ngrok.io. This URL forwards to your localhost:3000. Use it as the webhook’s notificationUrl.
JavaScript
Other tunneling options
| Tool | Notes |
|---|---|
| Cloudflare Tunnel | Free; persistent named tunnels available; integrates with Cloudflare’s CDN |
| Tailscale Funnel | Free for personal use; requires Tailscale account |
| localtunnel | Free; ephemeral URLs |
| Custom on a public VPS | If your team has a dedicated dev VPS, you can set up nginx + Let’s Encrypt to proxy to your machine |
Important: tunnel URLs change
Most tunneling tools assign a fresh URL each time you restart the tunnel (ngrok’s free tier rotates URLs). After every tunnel restart, update the webhook subscription’snotificationUrl via PUT /api/Webhook/{id}.
A common dev-loop pattern: a small script that starts the tunnel, captures the new URL, and updates the webhook subscription automatically:
JavaScript
Step 2: deploy the receiver
Your local webhook receiver needs to:- Listen on the local port the tunnel forwards to.
- Verify the signature on incoming requests.
- Acknowledge quickly with
200 OK. - Process (or queue for processing) the event.
JavaScript
x-raise-signature above) and the algorithm (sha256/hex) are placeholders — confirm against actual deliveries before relying on them. See Signature Verification.
Step 3: trigger test events
The fastest way to generate webhook events: submit test-mode donations throughPOST /api/Raise/give. Each successful submission fires the events your subscription is listening for.
Generate a test payment method
cURL
paymentMethodId usable for test-mode submissions. Re-use the same token across many test donations.
Submit a test donation
JavaScript
Generate other event types
Different event types fire from different actions:| Event family | How to trigger |
|---|---|
| Gift events | Submit a donation via POST /api/Raise/give; refund via POST /api/Gift/{id}/refund; delete via DELETE /api/Gift/{id} |
| RecurringGift events | Submit a donation with isRecurring: true; update via PUT /api/RecurringGift/{id}; cancel via PUT /api/RecurringGift/{id}/cancel |
| Donor events | Donor-creating donations trigger create events; PATCH /api/Donor/{id} triggers updates; POST /api/Donor/{id}/archive triggers a state change |
| Campaign events | POST /api/Campaign, PUT /api/Campaign/{id}, DELETE /api/Campaign/{id} |
Step 4: verify what arrives
For each test event, confirm two things: the event arrived at your local receiver, and Raise considers it successfully delivered.Local receiver inspection
Your local receiver should log every incoming event. Inspect the logs to confirm:- The event arrived
- The signature verified
- The payload shape matches what you expected
- Your processing logic ran without errors
Raise-side log inspection
Raise’s webhook log endpoints show the platform’s view of the delivery:cURL
| Field | What to confirm |
|---|---|
httpStatusCode | Your receiver returned 200 (or what you intended) |
success | Raise recorded the delivery as successful |
serverResponse | The body your receiver returned, captured by Raise |
eventTypeDisplay | The label for this event type — useful for confirming the integer-to-label mapping |
contextId | The ID of the entity that triggered the event |
success: "No", the issue is in your response — likely a status code other than 200 or a delayed response that exceeded Raise’s timeout.
If the Raise log shows success: "Yes" but your receiver doesn’t log the event, the receiver isn’t reachable at the URL configured (likely a tunneling issue).
Step 5: troubleshoot delivery
The most common local-dev issues and how to diagnose them:“I’m not receiving any events”
| Check | What to verify |
|---|---|
| Is the tunnel running? | curl https://your-tunnel.ngrok.io/raise-webhooks should hit your local server. If not, the tunnel is down. |
| Is the webhook subscription pointing at the right URL? | GET /api/Webhook/{id} and confirm notificationUrl matches the current tunnel URL. |
| Is the subscription active? | status: 1 (active) — if 2 (inactive), no events fire. |
| Is the event type subscribed? | eventTypesList must include the integers for the events you’re firing. |
| Are events actually firing? | Confirm via GET /api/Webhook/log/list — if no logs exist, no events fired. |
| Is the test mode correct? | If you’re submitting isTestMode: true donations, the resulting events should still fire — but confirm your subscription isn’t filtering them out (the spec doesn’t show a test-mode filter, so subscriptions receive both). |
”I’m receiving events but signature verification fails”
| Check | What to verify |
|---|---|
| Are you reading the raw body before any JSON parsing? | The signature is over the raw bytes. If a middleware parses to JSON first, re-serialized bytes won’t match. |
| Is the header name correct? | Inspect the request headers your local receiver sees. The header name isn’t formally documented in the spec — confirm against actual deliveries. |
| Is the algorithm right? | Try sha256 first, then sha1, then variants. Try hex vs base64 digest encoding. |
| Is the secret correct? | Confirm your local WEBHOOK_SECRET env var matches the securityToken on the subscription record. |
| Are you including any prefix or suffix? | Some webhook systems include a timestamp before the body (${ts}.${body}). Inspect the signature header format for any patterns. |
”Raise shows success but my receiver returned an error”
IfhttpStatusCode in the Raise log shows 200 but your local logs show errors, the response was sent before the error happened. Common cause: async processing inside the request handler — res.status(200) returns before the actual processing fails.
This is actually the correct pattern (acknowledge quickly, process asynchronously) — the “error” is downstream of the webhook reception. Investigate the processing pipeline separately.
”Multiple log entries for the same event”
IfGET /api/Webhook/{id}/log/list shows multiple entries with the same contextId, Raise retried the delivery. Check the timestamps:
- If the first attempt failed (
success: "No") and the second succeeded (success: "Yes"), retry worked correctly. - If multiple attempts all failed, the receiver is consistently failing — investigate the receiver-side logs for the cause.
Multi-customer testing
For partner integrations that serve multiple customers, test the multi-customer dispatch logic locally:JavaScript
notificationUrl includes the customer ID in the path. The receiver looks up the per-customer secret to verify the signature.
For testing this locally, create multiple test webhook subscriptions — one per simulated customer — each pointing at a different path under the same tunnel URL.
Capturing payloads for offline testing
Once you’ve confirmed end-to-end webhook delivery works, capture real payloads for offline testing. Save them to fixture files and replay them through your processing logic without needing the tunnel.JavaScript
JavaScript
Cleaning up after development
When you’re done with a development session:Stop the tunnel
Free the port and disconnect the tunnel to avoid leaving an open public URL pointing at your machine.
Set the subscription to inactive
PUT /api/Webhook/{id} with status: 2 to pause event delivery until your next session. This prevents events from queuing up at a tunnel URL that no longer works.Or delete the subscription entirely
If the work is complete,
DELETE /api/Webhook/{id} to remove the subscription. Keeps the customer’s webhook subscription list clean.Where to go next
Signature Verification
The verification pattern you’re testing locally.
Idempotency and Safe Reprocessing
Test the dedup logic by re-firing the same event.
Retry Behavior
Simulate retry scenarios locally by returning
5xx from the receiver.Process a Donation
The donation flow that fires the events you’re testing.