Skip to main content
Test and debug inbound email routing locally using the AhaSend CLI’s WebSocket-based route listener. Before running the examples, log in with an API key that has the read, write and delete route scopes for all domains, and 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
Every flag on this page is listed in the 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.
The API key needs the 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:
The note appears before the command connects, so you see the ID even when connecting then fails.

Existing Routes

With --route-id, the command listens through a route you created. A disabled route gets no events, so enable it first:
While you listen, the route’s own URL keeps receiving every email it matches, the test events of 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 next routes 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-id turns streaming on again.

Event Type

  • message.routing - Triggered when an inbound email matches a route
See the route payload for its fields.

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 prints Forwarding 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 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

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.
A disabled route gets no event: the API answers that it is queued, and then drops it.

Finding Route IDs

Temporary routes do not appear in the list. Their ID is in the listener’s banner and in the note on stderr.

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

Each event shows its time, its type and its data as indented JSON.

Slim Output

Each event shows its time, its type, and the sender, recipient and subject of the email.

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.

Use Cases

Testing Inbound Email Processing

Support Ticket System

Email-to-Task Integration

Best Practices

  1. Use Temporary Routes for Testing: Create temporary routes with wildcards during development
  2. Verify Signatures: Always validate webhook signatures in production
  3. Use listeners for development: Configure a real route URL for production
  4. Test with Trigger Command: Use trigger to test without sending real emails
  5. Clean Up: Delete temporary routes when you finish testing

Troubleshooting

WebSocket Connection Failed: Check network connectivity and firewall settings. The listener connects over wss:// 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.

Next Steps