> ## Documentation Index
> Fetch the complete documentation index at: https://ahasend.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> AhaSend stores all message data in the EU. See /facts.
> The API base URL is https://api.ahasend.com. Send the API key in the Authorization: Bearer header. See /api-reference/authentication.
> Use send.ahasend.com for SMTP. Ports 25, 587 and 2525 use STARTTLS. Port 465 is not supported. See /smtp.
> For send-only integrations, create an API Key v2 under Credentials → Add and scope it to messages:send:{your-domain}. See /send-api/credentials and /api-reference/scopes.

# CLI Examples: Scripts and CI/CD Automation

> Copy-paste AhaSend CLI scripts for order confirmations, password resets, alerts, GitHub Actions deploy notifications, webhook event processing, and load tests.

These AhaSend CLI examples show common email sending and management tasks, including sandbox tests and automation.

Before running the examples, [log in](https://ahasend.com/docs/cli/authentication.md) with an API key whose scopes cover the commands, and replace `example.com` in the sender addresses with your verified domain. Create the HTML, CSV, JSON and attachment files that an example names. The scripts use Bash, and the ones that read JSON need [jq](https://jqlang.org). Sends use `--sandbox`, so no email is delivered.

## Transactional Emails

### Order Confirmation

```bash theme={null}
# Single order confirmation
ahasend messages send --sandbox \
  --from orders@example.com \
  --to customer@example.com \
  --subject "Order #12345 Confirmed" \
  --html-file order-confirmation.html \
  --global-substitutions order-data.json \
  --attach invoice.pdf \
  --idempotency-key "order-12345"
```

**order-data.json:**

```json theme={null}
{
  "order_id": "12345",
  "customer_name": "John Doe",
  "total": "$149.99",
  "items": [
    {"name": "Product A", "quantity": 2, "price": "$49.99"},
    {"name": "Product B", "quantity": 1, "price": "$50.01"}
  ],
  "delivery_date": "<delivery date>"
}
```

The idempotency key makes a retry safe: for 24 hours, the API answers a request with the same key with the stored result of the first one instead of sending it again.

### Password Reset

```bash theme={null}
# Secure password reset email
ahasend messages send --sandbox \
  --from security@example.com \
  --to user@example.com \
  --subject "Password Reset Request" \
  --html-file password-reset.html \
  --global-substitutions reset-data.json \
  --header "X-Priority: high" \
  --track-clicks=false \
  --idempotency-key "reset-user123-$(date +%s)"
```

### Account Verification

```bash theme={null}
# Save global substitutions in a file (the flag takes a path).
cat > verification-data.json <<'JSON'
{"verify_url":"https://example.com/verify?token=TEST_TOKEN"}
JSON
# New user verification
ahasend messages send --sandbox \
  --from noreply@example.com \
  --to newuser@example.com \
  --subject "Verify Your Email Address" \
  --html "<h2>Welcome!</h2><p>Please verify your email by clicking <a href='{{verify_url}}'>here</a></p>" \
  --text "Welcome! Please verify your email by visiting: {{verify_url}}" \
  --global-substitutions verification-data.json \
  --tag signup,verification
```

`--tag` can be given several times, and it also splits on commas, so `--tag signup,verification` sets two tags.

## Lifecycle Notifications

### Account Activity Digest

```bash theme={null}
#!/bin/bash
# send-digest.sh

HTML_FILE="account-digest.html"
RECIPIENTS="account-users.csv"
SUBJECT="Your Account Activity Digest"

# Send the digest with tracking
STATUS=0
ahasend messages send --sandbox \
  --from digest@example.com \
  --recipients "$RECIPIENTS" \
  --subject "$SUBJECT" \
  --html-file "$HTML_FILE" \
  --track-opens=true \
  --track-clicks=true \
  --tag account-digest,transactional \
  --max-concurrency 5 \
  --progress \
  --show-metrics || STATUS=$?

if [ "$STATUS" -ne 0 ]; then
  echo "The send failed with exit code $STATUS" >&2
  exit "$STATUS"
fi

# Check delivery statistics for the last 7 days, by hour
ahasend stats deliverability \
  --from-time 7d \
  --group-by hour
```

The send exits with code 0 only when every recipient was sent. When some recipients were not sent, stderr names the file in `~/.ahasend/` that holds them, which you can pass back to `--recipients`. [Exit codes](https://ahasend.com/docs/cli/configuration.md#exit-codes) lists what each other code means.

### Account Feature Notifications

```bash theme={null}
# Notify account users about an enabled feature
for segment in vip regular trial; do
  echo "Sending to $segment segment..."

  ahasend messages send --sandbox \
    --from notifications@example.com \
    --recipients "${segment}-users.csv" \
    --subject "{{first_name}}, Your Account Feature Is Ready" \
    --html-file "launch-${segment}.html" \
    --global-substitutions launch-data.json \
    --tag "account-feature,${segment}" \
    --max-concurrency 3 \
    --progress

  sleep 60  # Stagger sends
done
```

### Pending Order Reminder

```bash theme={null}
#!/bin/bash
# pending-order-reminder.sh

# Query pending orders from database
# ... database query logic ...

# Create recipients file
cat > pending-orders.json << 'EOF'
[
  {
    "email": "shopper@example.com",
    "name": "Jane Shopper",
    "substitution_data": {
      "first_name": "Jane",
      "cart_value": "$234.99",
      "items_count": 3,
      "cart_url": "https://example.com/cart/restore/abc123",
      "discount_code": "COMEBACK10"
    }
  }
]
EOF

# Send the reminders two hours from now
ahasend messages send --sandbox \
  --from shop@example.com \
  --recipients pending-orders.json \
  --subject "{{first_name}}, your order is waiting for confirmation" \
  --html-file pending-order.html \
  --schedule 2h \
  --tag "pending-order,recovery,automated"
```

`--schedule` takes a relative time, such as `2h` from now, or an RFC 3339 time. The API takes a time in the future and at most 7 days ahead.

## Notification Systems

### Real-Time Alerts

```bash theme={null}
#!/bin/bash
# send-alert.sh

ALERT_TYPE=${1:-info}
ALERT_MESSAGE=${2:-"Docs alert test"}
RECIPIENTS=${3:-test@example.com}

case $ALERT_TYPE in
  critical)
    SUBJECT="CRITICAL ALERT"
    PRIORITY="high"
    ;;
  warning)
    SUBJECT="Warning"
    PRIORITY="normal"
    ;;
  info)
    SUBJECT="Information"
    PRIORITY="low"
    ;;
  *) echo "Use critical, warning, or info"; exit 1 ;;
esac

ahasend messages send --sandbox \
  --from alerts@example.com \
  --to "$RECIPIENTS" \
  --subject "$SUBJECT: $ALERT_MESSAGE" \
  --text "$(printf '%s\n\nTime: %s\nSystem: Test' "$ALERT_MESSAGE" "$(date)")" \
  --header "X-Priority: $PRIORITY" \
  --tag "alert,${ALERT_TYPE},automated"
```

### Daily Reports

```bash theme={null}
#!/bin/bash
# daily-report.sh

# Generate report data
generate_report() {
  echo "{"
  echo "  \"date\": \"$(date +%Y-%m-%d)\","
  echo "  \"total_users\": 15234,"
  echo "  \"new_signups\": 87,"
  echo "  \"revenue\": \"\$12,456.78\","
  echo "  \"top_products\": ["
  echo "    {\"name\": \"Product A\", \"sales\": 234},"
  echo "    {\"name\": \"Product B\", \"sales\": 189}"
  echo "  ]"
  echo "}"
}

# Generate and save report data
generate_report > report-data.json

# Send the daily report one hour from now
ahasend messages send --sandbox \
  --from reports@example.com \
  --recipients executives.csv \
  --subject "Daily Report - $(date +%Y-%m-%d)" \
  --html-file daily-report.html \
  --global-substitutions report-data.json \
  --attach "reports/detailed-$(date +%Y%m%d).pdf" \
  --schedule 1h \
  --tag "report,daily,automated"
```

## Webhook Integration

### Process Webhook Events

```bash theme={null}
# Run your local, signature-verifying receiver first; see the webhook guide.
ahasend webhooks listen \
  --forward-to http://localhost:8080/webhook \
  --events message.bounced,message.failed
```

The receiver processes `payload.type` and `payload.data.recipient`. The listener's own output is text for people to read, so process the events in the receiver. See [webhook verification and processing](https://ahasend.com/docs/integrations/webhooks.md). Your receiver does not need to suppress bouncing addresses: AhaSend suppresses them itself, for a time that depends on the cause, as [suppression durations](https://ahasend.com/docs/bounces.md#suppression-duration) lists.

### Webhook Testing Pipeline

```bash theme={null}
#!/bin/bash
# test-webhook-integration.sh
set -euo pipefail

# 1. Create a disabled test webhook; --output json prints the API's webhook object
WEBHOOK_ID=$(ahasend webhooks create --non-interactive --enabled=false \
  --url https://example.com/test-webhook \
  --all-events \
  --name "Integration test webhook" \
  --output json | jq -er '.id')

cleanup() {
  ahasend webhooks update "$WEBHOOK_ID" --enabled=false || true
  if [ -n "${LISTENER_PID:-}" ]; then kill "$LISTENER_PID" 2>/dev/null || true; fi
  ahasend webhooks delete "$WEBHOOK_ID" --force
}
trap cleanup EXIT

echo "Created webhook: $WEBHOOK_ID"

# 2. Start listener with forwarding (a local handler must already be running)
ahasend webhooks listen \
  --webhook-id "$WEBHOOK_ID" \
  --forward-to http://localhost:3000/test-webhook \
  --verbose &
LISTENER_PID=$!

# 3. Trigger test events
sleep 2
# Enable only after the listener connects; disabled webhooks emit no events.
ahasend webhooks update "$WEBHOOK_ID" --enabled
ahasend webhooks trigger "$WEBHOOK_ID" --all-events

# 4. Send test email
ahasend messages send \
  --from test@example.com \
  --to test@example.com \
  --subject "Webhook Test" \
  --text "Testing webhook integration" \
  --sandbox

# 5. Wait and cleanup
sleep 5
echo "Integration test complete"
```

`--non-interactive` makes `webhooks create` fail on a missing flag instead of asking for it, and `webhooks delete --force` skips the confirmation prompt, which cancels the command when stdin holds no answer.

## Automation Scripts

### Exit Codes in Scripts

Every failure exits with a nonzero code, in every output format, so `set -e`, `if`, and CI steps stop on a failed command. The code tells the kind of failure, as [exit codes](https://ahasend.com/docs/cli/configuration.md#exit-codes) lists. With `--output json`, stdout holds the API's response, or the error, as [JSON output](https://ahasend.com/docs/cli/configuration.md#json-output) describes.

```bash theme={null}
# Read a domain's DNS status, and show the JSON error on failure
STATUS=0
ahasend domains get example.com --output json > domain.json || STATUS=$?

case $STATUS in
  0) jq -r '.dns_valid' domain.json ;;
  5) echo "example.com is not in this account" >&2; exit 5 ;;
  *) echo "domains get failed with exit code $STATUS:" >&2; cat domain.json >&2; exit "$STATUS" ;;
esac
```

### CI/CD Email Notifications

```yaml theme={null}
# .github/workflows/deploy-notification.yml
name: Deployment Notification

on:
  deployment_status:

jobs:
  notify:
    if: github.event.deployment_status.state == 'success'
    runs-on: ubuntu-latest
    steps:
      - name: Install AhaSend CLI
        run: |
          FILE=ahasend-linux-amd64
          curl -fsSLO "https://github.com/AhaSend/ahasend-cli/releases/latest/download/$FILE"
          curl -fsSLO https://github.com/AhaSend/ahasend-cli/releases/latest/download/checksums.txt
          sha256sum --ignore-missing -c checksums.txt
          chmod +x "$FILE"
          mkdir -p ~/.local/bin
          mv "$FILE" ~/.local/bin/ahasend
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"

      - name: Send deployment notification
        env:
          AHASEND_API_KEY: ${{ secrets.AHASEND_API_KEY }}
          AHASEND_ACCOUNT_ID: ${{ secrets.AHASEND_ACCOUNT_ID }}
          DEPLOY_ENVIRONMENT: ${{ github.event.deployment.environment }}
        run: |
          ahasend --api-key "$AHASEND_API_KEY" --account-id "$AHASEND_ACCOUNT_ID" messages send --sandbox \
            --non-interactive \
            --from ci@example.com \
            --to team@example.com \
            --subject "Deployment Successful: $DEPLOY_ENVIRONMENT" \
            --html "<h2>Deployment Complete</h2>
                    <p>Environment: $DEPLOY_ENVIRONMENT</p>
                    <p>Commit: $GITHUB_SHA</p>
                    <p>Deployed by: $GITHUB_ACTOR</p>
                    <p>Time: $(date)</p>" \
            --tag "deployment,ci,notification"
```

The install step checks the binary against the release's `checksums.txt`, as [binary download](https://ahasend.com/docs/cli/installation.md#binary-download) does, and adds its folder to the `PATH` of the later steps. The CLI reads no environment variables for credentials, so the send step passes the secrets with `--api-key` and `--account-id`. It gets the deployment's environment name through `env:`, so the shell reads it as a variable and never runs it as code. The step fails when the send fails, because the CLI exits with a nonzero code.

### Database Backup Notifications

```bash theme={null}
#!/bin/bash
# backup-notification.sh

# Pass the path of your database dump.
BACKUP_SOURCE=${1:-database.sql}
BACKUP_FILE="backup-$(date +%Y%m%d-%H%M%S).sql.gz"

if gzip -c "$BACKUP_SOURCE" > "$BACKUP_FILE"; then
  BACKUP_SIZE=$(wc -c < "$BACKUP_FILE")
  SUBJECT="Backup Successful"
  MESSAGE=$(printf 'Backup completed.\nFile: %s\nBytes: %s' "$BACKUP_FILE" "$BACKUP_SIZE")
  TAGS="backup,success"
else
  BACKUP_STATUS=$?
  SUBJECT="Backup Failed"
  MESSAGE="Database backup failed with error code: $BACKUP_STATUS"
  TAGS="backup,failure,alert"
fi

# Send notification
ahasend messages send --sandbox \
  --from backup@example.com \
  --to admin@example.com \
  --subject "$SUBJECT - $(date +%Y-%m-%d)" \
  --text "$MESSAGE" \
  --tag "$TAGS"
```

### User Onboarding Sequence

```bash theme={null}
#!/bin/bash
# onboarding-sequence.sh
# Stop at the first send that fails.
set -e

USER_EMAIL=${1:-test@example.com}
USER_NAME=${2:-"Test User"}
# jq safely escapes the name and writes the file expected by the CLI.
jq -n --arg name "$USER_NAME" '{name:$name}' > onboarding-data.json

# Welcome email (immediate)
ahasend messages send --sandbox \
  --from welcome@example.com \
  --to "$USER_EMAIL" \
  --subject "Welcome to Our App, $USER_NAME!" \
  --html-file templates/welcome.html \
  --global-substitutions onboarding-data.json \
  --tag "onboarding,day0,welcome"

# Day 1: Getting started guide
ahasend messages send --sandbox \
  --from tips@example.com \
  --to "$USER_EMAIL" \
  --subject "Getting Started with Our App" \
  --html-file templates/getting-started.html \
  --global-substitutions onboarding-data.json \
  --schedule 1d \
  --tag "onboarding,day1,guide"

# Day 3: Feature highlights
ahasend messages send --sandbox \
  --from tips@example.com \
  --to "$USER_EMAIL" \
  --subject "Discover Our Best Features" \
  --html-file templates/features.html \
  --global-substitutions onboarding-data.json \
  --schedule 3d \
  --tag "onboarding,day3,features"

# Day 6: Check-in
ahasend messages send --sandbox \
  --from support@example.com \
  --to "$USER_EMAIL" \
  --subject "How's Everything Going?" \
  --html-file templates/checkin.html \
  --global-substitutions onboarding-data.json \
  --schedule 6d \
  --tag "onboarding,day6,checkin"

echo "Onboarding sequence scheduled for $USER_EMAIL"
```

The API takes a schedule at most 7 days ahead, so a longer sequence needs a scheduler of its own, such as cron.

## Performance Testing

### Load Testing Script

```bash theme={null}
#!/bin/bash
# load-test.sh

# Configuration
TOTAL_EMAILS=${TOTAL_EMAILS:-250}
CONCURRENCY=${CONCURRENCY:-3}

echo "Starting load test: $TOTAL_EMAILS emails"

# Generate test recipients
echo "email,name,test_id" > test-recipients.csv
for i in $(seq 1 "$TOTAL_EMAILS"); do
  echo "test${i}@example.com,Test User ${i},TEST-${i}" >> test-recipients.csv
done

# Each request holds up to 100 recipients, and --max-concurrency sends
# several requests at once. The results go to stdout; the metrics and
# notices go to stderr.
STATUS=0
ahasend messages send \
  --from loadtest@example.com \
  --recipients test-recipients.csv \
  --subject "Load Test {{test_id}}" \
  --text "This is load test message {{test_id}}" \
  --sandbox \
  --max-concurrency "$CONCURRENCY" \
  --show-metrics \
  --output json > load-test-results.json 2> load-test-metrics.txt || STATUS=$?

cat load-test-metrics.txt
echo "Exit code: $STATUS"

# Count the recipients by status. A send of more than 100 recipients prints
# a JSON array with the API's response for each request.
jq '[if type == "array" then .[] else . end | .data[]? | .status]
    | group_by(.) | map({status: .[0], count: length})' load-test-results.json

# Cleanup
rm test-recipients.csv
```

## Multi-Environment Management

### Environment-Specific Sending

```bash theme={null}
#!/bin/bash
# deploy-emails.sh
set -e

ENVIRONMENT=${1:-}

case $ENVIRONMENT in
  production)
    PROFILE="production"
    FROM="noreply@example.com"
    RECIPIENTS="all-users.csv"
    ;;
  staging)
    PROFILE="staging"
    FROM="test@staging.example.com"
    RECIPIENTS="test-users.csv"
    ;;
  development)
    PROFILE="development"
    FROM="dev@dev.example.com"
    RECIPIENTS="dev-team.csv"
    ;;
  *)
    echo "Unknown environment: $ENVIRONMENT"
    exit 1
    ;;
esac

# Send with environment-specific profile
ahasend messages send --sandbox \
  --profile "$PROFILE" \
  --from "$FROM" \
  --recipients "$RECIPIENTS" \
  --subject "[$ENVIRONMENT] System Update" \
  --html-file update-notification.html \
  --tag "update,${ENVIRONMENT}"

echo "Emails sent to $ENVIRONMENT environment"
```

A profile that does not exist fails with exit code 3 before any request is sent.

## Next Steps

* [Learn about batch processing](https://ahasend.com/docs/cli/batch-processing.md)
* [Configure webhooks](https://ahasend.com/docs/cli/commands/webhooks.md)
* [Explore the API](https://ahasend.com/docs/api-reference.md)

## Related Guides

* [Install the CLI](https://ahasend.com/docs/cli/installation.md), [CLI authentication](https://ahasend.com/docs/cli/authentication.md), [REST sending guide](https://ahasend.com/docs/send-api/send-email.md).
* [SMTP settings](https://ahasend.com/docs/smtp.md), [Node.js SDK](https://ahasend.com/docs/guides/nodejs-sdk.md), [Go SDK](https://ahasend.com/docs/guides/go-sdk.md).
* [CLI configuration and output](https://ahasend.com/docs/cli/configuration.md), [API authentication](https://ahasend.com/docs/api-reference/authentication.md), [Permission scopes](https://ahasend.com/docs/api-reference/scopes.md).
* [Domain setup](https://ahasend.com/docs/domains.md), [Sandbox behavior](https://ahasend.com/docs/send-api/sandbox.md), [Tracking setup](https://ahasend.com/docs/tracking/open-tracking.md).
* [Suppression durations](https://ahasend.com/docs/bounces.md#suppression-duration).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.