Skip to main content
By the end of this guide, you’ll have a webhook inbox that captured a request, and a Vitest or pytest test that waits for that request and asserts on its body.
  • The CLI (npm install -g devhelm) or curl, plus jq, for the steps; @devhelm/sdk with Vitest (ESM only), or devhelm with pytest and httpx, for the test
  • A free inbox slot. Inboxes are counted per organization: 3 on the Free testing plan, 10 on Developer
The testing API needs an API key and a workspace ID. Export the key, look up the workspace, then export its ID:
The CLI and Python SDK read both variables. The TypeScript SDK reads DEVHELM_WORKSPACE_ID and takes the key in its constructor. With curl, send the workspace as a header:

Capture a request

1

Create an inbox

The URL is https://api.devhelm.io/api/v1/ingest/<public-token>. It accepts any method and any path below the token, with no auth. The token is the secret: anyone with the URL can send to the inbox.
2

Record the start time, then send

Take SINCE before anything sends: wait returns the oldest request received at or after it. Without it, the CLI and SDKs use the clock at the moment you call wait, and a request that already arrived is missed.The curl stands in for your service. The inbox replies 200 with an empty body plus X-Event-Id and X-Inbox-Id. Storage is asynchronous, so getting the event by X-Event-Id can return 404 for about a second. Wait handles that.
3

Wait and assert

Wait blocks until a matching request is stored, then returns it. A match already stored returns at once. If nothing matches within timeoutMs (default 30000), the API returns 408 with code WAIT_TIMEOUT. REST returns {"event": {...}} with no data wrapper; the CLI prints the event object. jq -e exits non-zero when the expression is false, so a CI step fails like an assertion.The event also carries query (repeated keys kept), headers (names lowercased, nothing redacted), sha256 of the raw body, sourceIp, and receivedAt. Both SDKs parse body with event.json(). Signature checks hash the raw bytes instead; see Verify webhook signatures.
4

Delete the inbox

Deleting frees the slot and removes the inbox’s events. The URL then returns 404.

Write the test

The same flow as one test file, with cleanup. The fetch or httpx.post call stands in for the code under test: replace it with the call that makes your service send its webhook.
Run it with npx vitest run first-inbox.test.ts or pytest test_first_inbox.py. One test passes. The runner timeout (45 s) is longer than the wait timeout (30 s), so a missing request fails with the API’s 408 instead of the runner killing the test. The test owns its inbox, so no earlier request can match. In CI, use one inbox per job and a path per test; see Testing in CI.

Troubleshooting

  • since was taken after the send, or not passed. Take it before the trigger.
  • The request path does not start with the pathPrefix you passed. The match is case-sensitive.
  • The sender failed. Check the status it got back.
The CLI and SDKs fall back to workspace 1 when DEVHELM_WORKSPACE_ID is unset, which gives the 404. A curl call without the x-phelm-workspace-id header gets the 400. Set both from the setup step.
Your organization has used all its inbox slots. Find unused ones with devhelm inboxes list and delete them. See Limits, quotas, and errors.

Next steps

Webhook inboxes

Stored fields, mock replies, listing, and raw downloads.

Wait for a webhook in a test

The Nth request, negative assertions, and per-test paths.

What is webhook testing?

Capture vs polling vs tunnels, and why one inbox per test.