CLI (evctl)
evctl is the EmailValidator command-line tool. It exists so you can develop and test a webhook
handler on localhost — without a public URL, a tunnelling service, or a live validation.
On this page
Install
dotnet tool install -g evctl
Two commands:
| Command | What it does |
|---|---|
evctl listen |
Subscribes to your account's event stream and forwards real events to a local URL, already signed |
evctl trigger |
Fabricates one signed event and POSTs it to a local URL — no account, no job, no credits |
Both send the same three headers the production worker sends — X-Signature, X-Ev-Event and
X-Ev-Delivery — so code written against the CLI never reads a header production does not send.
Forward real events to your local server
evctl listen --forward-to http://localhost:5000/your-hook --api-key ev_your_key
You can also set EV_API_KEY in your environment and omit the --api-key flag:
export EV_API_KEY=ev_your_key
evctl listen --forward-to http://localhost:5000/your-hook
How it works
evctl listen connects to the API's server-sent event stream, prints a signing secret, and
forwards each event to your local endpoint — already signed — so your real
signature-verification code runs against real payloads.
On startup it prints a session signing secret:
Session signing secret: whsec_example_session_secret
Set EV_WEBHOOK_SECRET=whsec_example_session_secret to verify locally.
Ready! Forwarding to http://localhost:5000/your-hook
Use that whsec_ value as the secret in whichever verification snippet fits your stack (see
Webhooks). It is ephemeral — a new one is generated each time
evctl listen starts.
For each incoming event it prints the event type, the HTTP status your handler returned, and the round-trip latency:
14:02:11 verify.completed → 200 (12ms)
No ngrok, no reverse proxy, no firewall changes. Your handler never needs to be reachable from the internet.
Fabricate an event with evctl trigger
listen still needs a real validation to happen. trigger does not: it builds a synthetic
envelope of the shape the API emits, signs it, and delivers it immediately.
evctl trigger test --to http://localhost:3000/webhook
evctl trigger <event> [--to <url>] [--secret <secret>]
| Argument / option | Value |
|---|---|
<event> |
verify.completed, batch.completed, or test |
--to |
The local URL to POST to. Defaults to http://localhost:3000/webhook |
--secret |
Signing secret. Falls back to EV_WEBHOOK_SECRET, then to a freshly generated one that is printed for you |
An unknown event name is refused with a non-zero exit code rather than delivered, so a typo in a script fails loudly.
The output is one line, and the exit code is 0 only when your handler answered 2xx — which
makes it usable directly in a test script:
test → 200 (7ms)
The envelope is indistinguishable in shape from a real one, including an evt_-prefixed id
derived the same way, so this is a genuine exercise of your handler:
evctl trigger verify.completed --to http://localhost:3000/webhook --secret whsec_example_local_secret
Reuse one secret across runs by exporting it once:
export EV_WEBHOOK_SECRET=whsec_example_local_secret
evctl trigger test
evctl trigger verify.completed
evctl trigger batch.completed
Which one do I want?
- Writing signature verification, or a handler, from scratch →
evctl trigger. It is instantaneous, free, and needs no API key. - Checking that your handler copes with your own real data →
evctl listen, then run a validation. - Checking that our delivery reaches a deployed endpoint →
POST /api/v1/webhooks/test, which signs with your account secret and returns theevent_idthe delivery will carry. See Webhooks.