Capture.
Any method, any path. Method, path, query string, headers and body are stored for every request: Stripe, GitHub, Auth0, or a partner's "we'll send you a webhook".
WEBHOOK TESTING · BETA
A private URL that accepts any method on any path. Paste it into Stripe, inspect what arrives, and let your test block on one API call until the webhook lands, or fail with a 408.
REST API, plus TypeScript and Python SDKs.
WHAT IT DOES
The three jobs of a webhook test, each one an API call.
Any method, any path. Method, path, query string, headers and body are stored for every request: Stripe, GitHub, Auth0, or a partner's "we'll send you a webhook".
Open a request to read the JSON body, every header and the query string. See whether the provider sent what your handler expects.
One call blocks until a matching request arrives. It turns "Stripe should have called us" into an assertion.
THE WAIT API
Trigger the action, then make one call. It returns as soon as a matching request is stored. No polling loop, no sleep(10).
import { test, expect } from '@playwright/test';
const API = 'https://api.devhelm.io/api/v1';
test('Stripe calls our webhook after checkout', async ({ page, request }) => {
test.setTimeout(60_000);
await page.goto('https://staging.acme.dev/pricing');
await page.getByRole('button', { name: 'Start Pro' }).click();
// ...complete checkout with Stripe's 4242 test card...
const res = await request.post(`${API}/webhook/inboxes/${process.env.INBOX_ID}/wait`, {
headers: {
Authorization: `Bearer ${process.env.DEVHELM_API_TOKEN}`,
'x-phelm-workspace-id': process.env.DEVHELM_WORKSPACE_ID!,
},
data: { timeoutMs: 30_000, http: { method: 'POST', pathPrefix: '/stripe' } },
timeout: 35_000,
});
expect(res.status(), 'Stripe never called the webhook').toBe(200);
});
curl -sS -X POST "https://api.devhelm.io/api/v1/webhook/inboxes/$INBOX_ID/wait" \
-H "Authorization: Bearer $DEVHELM_API_TOKEN" \
-H "x-phelm-workspace-id: $DEVHELM_WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{"timeoutMs":30000,"http":{"method":"POST","pathPrefix":"/stripe"}}'
# 200: {"event": …} the captured request (method, path, query, headers, body)
# 408: WAIT_TIMEOUT, nothing arrived within timeoutMs
- name: Wait for the Stripe webhook
run: |
curl -sS --fail-with-body -X POST "https://api.devhelm.io/api/v1/webhook/inboxes/${{ vars.INBOX_ID }}/wait" \
-H "Authorization: Bearer ${{ secrets.DEVHELM_API_TOKEN }}" \
-H "x-phelm-workspace-id: ${{ vars.DEVHELM_WORKSPACE_ID }}" \
-H "Content-Type: application/json" \
-d '{"timeoutMs":30000,"http":{"method":"POST","pathPrefix":"/stripe"}}'
PRIVATE BY DEFAULT
The URL you paste into Stripe is public, because providers cannot send a login. Reading what arrived is not: that needs your workspace.
Public sender
POST the ingest URL
Capture URL
stripe-staging
Private workspace
Authenticated read
CUSTOM REPLIES
Set the status code, content type and body your inbox returns, and hold the reply for up to 30 seconds, so you can see how a provider behaves when your endpoint says 200, 410 or 500, or answers slowly.
SETUP
Once the DevHelm team turns on your workspace, three steps get the inbox ready for your test suite.
In the dashboard. Your tests send it with your workspace ID; it is the only credential they need.
In the dashboard or through the API. You get a URL to paste into your provider.
Click "Send test webhook" in Stripe, or run curl -X POST <your-inbox-url>/stripe -d '{"ping":true}'. It shows up in the inspector and in your first wait call.
$ curl -sS -o /dev/null -w '%{http_code}\n' -X POST "$INBOX_URL/stripe" -d '{"ping":true}'
200FIT CHECK
It is a test inbox with a wait call. If that is the job, it is built for you.
PRICING
Testing is its own plan, on the same subscription as monitoring. Add it to any monitoring plan, Free included.
$0
$19 / mo
$79 / mo
$149 / mo
Enterprise: unlimited inboxes, domains and events, 365-day history, custom pricing. Stored events count webhook and email together. Annual billing: two months free.
FAQ
Waiting, privacy, Stripe and pricing.
A webhook tester gives you a URL that accepts HTTP requests and shows you what arrived, so you can see a provider's payload before your own handler exists. DevHelm's is private to your workspace, adds a wait API for CI, and lets you set the reply.
Trigger the action, then call POST /api/v1/webhook/inboxes/{id}/wait with a timeoutMs (default 30 s, maximum 120 s). It returns the request as soon as it is stored, or a 408 if none arrives. Your test asserts on what came back.
The wait call returns HTTP 408 with WAIT_TIMEOUT after your timeout. It never returns an empty 200, so a missing webhook fails your test with a clear reason.
Anyone with the URL can send to it, because providers can't sign in. Listing and reading events needs your DevHelm login or API key.
Yes. Set the status (200 to 599), content type, body and a reply delay of up to 30 seconds for each inbox, for example a 500 to see how a provider retries. Extra headers can be set through the API.
Yes for receiving them: paste the inbox URL into the Stripe dashboard and send a test event. No tunnel needed because the inbox is a public URL. If you need Stripe to reach your local handler, you still need a tunnel for that.
There is a free plan: 3 inboxes, 300 stored events a month and 3 days of history. Paid plans are $19, $79 and $149 per month, and annual billing gets two months free.
The beta is open by invitation. Book a 30-minute demo and we'll set up your workspace.
$ curl -sSf -X POST https://api.devhelm.io/api/v1/webhook/inboxes/$INBOX_ID/wait \
-H "Authorization: Bearer $DEVHELM_API_TOKEN" -H "x-phelm-workspace-id: $DEVHELM_WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{"timeoutMs":30000,"http":{"method":"POST","pathPrefix":"/stripe"}}'
✓ 200 · POST /stripe returned