messages:read for the domain of the route’s recipient, which routes listen and routes trigger need. Replace example.com in the recipients with a receiving domain: a domain in your account whose MX record points to AhaSend. Replace YOUR_RESOURCE_ID with the ID of one of your routes or of the temporary route the listener creates.
Overview
The CLI provides tools for:- Listening to inbound email events in real-time over a WebSocket connection
- Using existing routes or creating temporary routes with recipient patterns
- Forwarding events, signed, to local endpoints for development
- Displaying events in full or slim output format
- Triggering test route events
routes command reference.
Route Listener
Basic Usage
ahasend routes listen prints each event as text, whatever --output says. It runs until you press Ctrl+C or the connection closes. Give exactly one of --route-id and --recipient.
routes:write:all scope, and messages:read for the domain of the route’s recipient (messages:read:all, or messages:read:{example.com}). The recipient’s domain must be one of your domains, with its AhaSend MX record configured, for the route to receive email. With --recipient, the API checks this before it creates the route, and the command fails at once with the API’s message: with exit code 4 when the domain is not in your account (domain 'example.com' not found or not owned by this account), and with exit code 6 when its MX record is not configured (domain 'example.com' MX records are not configured).
When the connection starts, the command prints a banner with the route ID, the recipient pattern of a temporary route, the forwarding secret and, with --forward-to, the forwarding URL.
Attachment data is not sent over the WebSocket. Only email metadata and content are.
Temporary Routes
With--recipient, the API creates a temporary route named DevMode route for <API key label>. It is enabled, and routes 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:
Existing Routes
With--route-id, the command listens through a route you created. A disabled route gets no events, so enable it first:
routes trigger included. Listening also turns on streaming for the route, and streaming stays on after the command ends, as Events Without a Listener describes.
Events Without a Listener
A route 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 nextroutes listen --route-id of the same route 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 route:
- A temporary route is deleted.
- A route you created keeps posting emails to its URL. The next
routes listen --route-idturns streaming on again.
Event Type
message.routing- Triggered when an inbound email matches a route
Event Forwarding
Forward to Local Server
--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.
Event Headers
When the connection starts, the banner printsForwarding secret (signs forwarded events): <secret>. The command makes a new secret on each run, also with --route-id: it is not the secret of that route.
Forwarded requests carry the Standard Webhooks headers:
webhook-id- Unique message identifierwebhook-timestamp- Unix timestampwebhook-signature- HMAC-SHA256 signature for 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
routes trigger asks the API to send a test message.routing event to a route. Use the route ID that the listener’s banner shows to see the event in your terminal. The API key needs messages:read for the domain of the route’s recipient (messages:read:all, or messages:read:{example.com}); route scopes do not grant it.
Finding Route IDs
Integration Testing
Development Workflow
routes create prints the route’s signing secret once. Save it to verify the emails the route posts to its --url.
Output Formats
Standard Output
Slim Output
Connection and Exit Codes
The listener does not reconnect. It exits with:0when the server closes the connection normally, or when you press Ctrl+C7when the connection drops, or the server closes it with an error4when you decline to take over the stream from another connection
Use Cases
Testing Inbound Email Processing
Support Ticket System
Email-to-Task Integration
Best Practices
- Use Temporary Routes for Testing: Create temporary routes with wildcards during development
- Verify Signatures: Always validate webhook signatures in production
- Use listeners for development: Configure a real route URL for production
- Test with Trigger Command: Use trigger to test without sending real emails
- Clean Up: Delete temporary routes when you finish testing
Troubleshooting
WebSocket Connection Failed: Check network connectivity and firewall settings. The listener connects overwss:// and uses the proxy set by the HTTPS_PROXY, HTTP_PROXY and NO_PROXY environment variables.
Events Not Received: Verify that the route is enabled, that its pattern matches the recipient address, and that the domain’s MX record points to AhaSend.
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 route’s own secret. Each run prints a new secret.
Route Not Found: Check the route ID with ahasend routes list. Temporary routes do not appear there; use the ID from the listener’s banner.
Connection Lost: The listener does not reconnect. Start it again with --route-id and the same route ID to receive the kept events.

