Skip to main content
Test and debug webhook events locally without deploying to production using the AhaSend CLI. Before running the examples, log in with an API key that has the read, write and delete webhook scopes for all domains, and replace YOUR_RESOURCE_ID with the ID of one of your webhooks or of the temporary webhook the listener creates. The sandbox send in Development Workflow also needs the messages:send:all scope and your verified sender domain in place of yourdomain.com. The scripted trigger needs jq.

Overview

The CLI provides tools for:
  • Streaming webhook events to your terminal over a WebSocket connection
  • Forwarding events, signed, to a local endpoint
  • Triggering test events
  • Debugging webhook signatures and payloads
Every flag on this page is listed in the webhooks command reference.

Local Webhook Listener

Basic Usage

ahasend webhooks listen prints each event as text, whatever --output says. It runs until you press Ctrl+C or the connection closes.
The API key needs the webhooks:write:all scope. When the connection starts, the command prints a banner with the webhook ID, the forwarding secret and, with --forward-to, the forwarding URL. --events filters in the CLI: events of other types are neither shown nor forwarded. It takes event keys or types, such as opened or message.opened.

Temporary Webhooks

Without --webhook-id, the API creates a temporary webhook named DevMode webhook for <API key label>. webhooks list does not show it. It stays after the command ends, until you delete it or the API does, as Events Without a Listener describes. As soon as the API creates it, the command prints a note on stderr with its ID and the command that deletes it:
The note appears before the command connects, so you see the ID even when connecting then fails.

Existing Webhooks

With --webhook-id, the command listens through a webhook you created. A disabled webhook gets no events, so enable it first:
While you listen, the webhook’s own URL keeps receiving its events, the test events of webhooks trigger included. Listening also turns on streaming for the webhook, and streaming stays on after the command ends, as Events Without a Listener describes.

Events Without a Listener

A webhook with streaming on keeps it when no listener is connected: the API keeps its events for the next listener, up to the last 100. When 5 minutes pass with no new event, the API drops the kept events. The next webhooks listen --webhook-id of the same webhook prints the kept events first, each marked REPLAY:. When an event arrives while 100 events are already kept, the API drops the kept events and turns off streaming for the webhook:
  • A temporary webhook is deleted.
  • A webhook you created keeps sending events to its URL. The next webhooks listen --webhook-id turns streaming on again.

Available Events

The webhooks event types lists every event you can listen for and trigger. See the event reference for each payload.

Forwarding Events

Forward to a Local Endpoint

--forward-to sends each event to your handler as an HTTP POST with a JSON body. The URL must be HTTP or HTTPS with a host name and no user name or password; another URL fails with exit code 4. A plain HTTP URL for a host other than localhost or a loopback address prints a warning on stderr.
--skip-verify applies only to the --forward-to endpoint, and needs it: without --forward-to, the command fails with exit code 4. The connection to AhaSend is always verified.

Forwarding Secret and Signatures

When the connection starts, the banner prints Forwarding secret (signs forwarded events): <secret>. The command makes a new secret on each run, also with --webhook-id: it is not the secret of that webhook. Each forwarded request carries the Standard Webhooks headers:
  • webhook-id - Unique message identifier
  • webhook-timestamp - Unix timestamp
  • webhook-signature - HMAC-SHA256 signature for verification
To verify a forwarded event, use the full secret string, as printed, as the raw UTF-8 HMAC key. Do not base64-decode it. See signature verification.

Order and Delivery

The command forwards events one at a time, in the order they arrive, with their body byte for byte as the stream sent it, so the signature covers exactly what the API sent. A slow endpoint does not hold up the stream: up to 100 events, and up to 32 MiB of their data, wait for it, in order. When an event would go over either limit, the command does not forward it and says so on stderr. A forwarded request is not sent again. When the request fails, the command logs an error on stderr; when it gets a response outside the 2xx range, the command logs a warning. When the stream ends, the command waits for the events not yet forwarded. Press Ctrl+C to skip the wait.

Triggering Test Events

Development Testing

webhooks trigger asks the API to send test events to a webhook. Use the webhook ID that the listener’s banner shows to see the events in your terminal.
A disabled webhook gets no events: the API answers that they are queued, and then drops them.

Event Simulation Scenarios

Integration Testing

Development Workflow

Scripted Trigger

webhooks trigger exits nonzero when the API refuses the request, so a script with set -e stops on an API error:

Connection and Exit Codes

The listener does not reconnect. It exits with:
  • 0 when the server closes the connection normally, or when you press Ctrl+C
  • 7 when the connection drops, or the server closes it with an error
  • 4 when you decline to take over the stream from another connection
Other failures, such as an invalid flag or an API error while the command sets up the stream, exit as Exit Codes lists. When another connection holds the stream, the command asks on stderr whether to force a reconnect, and takes the stream over when you confirm, as Prompts describes. The other connection stays open but gets no more events. A message on the stream that the command cannot read, such as one that is not JSON, is skipped with a warning on stderr, and the command keeps listening.

Debugging

Output Options

Stdout holds the banner, the Session established line and the events, with separator lines between them. Notes, warnings, and the logs of --verbose and --debug go to stderr.

Common Issues

Signature Verification Failed: Use the forwarding secret of the current run, as the banner prints it, as the raw UTF-8 HMAC key. Do not base64-decode it, and do not use the webhook’s own secret. Each run prints a new secret. Events Not Received: Check that the webhook is enabled, that --events does not filter the events out, and that your local server is running. Run with --debug to see each forward and its response status. Connection Lost: The listener does not reconnect. Start it again with --webhook-id and the same webhook ID to receive the kept events.

Best Practices

  1. Start Simple: Test with single events before complex flows
  2. Use Sandbox Mode: Test with sandbox emails to avoid sending real messages
  3. Verify Signatures: Always validate webhook signatures in production
  4. Clean Up: Delete temporary webhooks when you finish testing
  5. Test Error Cases: Simulate failures to ensure proper error handling

Next Steps