AhaSend
Back to Blog

How to Import a Suppression List

Mark Kraakman
Mark Kraakman
Guides

If you're migrating from another ESP or need to seed your suppression list with addresses that should never receive email, AhaSend's Suppressions API lets you do that programmatically. This guide shows you how to bulk-import a suppression list using cURL, Python, and the Go SDK.

What are Suppressions?

A suppression list is your record of email addresses you should not send to - hard bounces, spam complaints, manual unsubscribes, or addresses you know are invalid. Sending to suppressed addresses hurts your sender reputation and, for hard bounces and complaints, may violate your terms of service with your ESP.

When migrating from another provider, importing your existing suppression list before sending your first email is one of the most important steps you can take. If you skip it, you'll repeat the bounces and complaints that built that list in the first place.

Exporting your suppression list from another provider

Before you can import into AhaSend, you need to get your existing suppression list out of your current ESP.

  • SendGrid: Use the Retrieve all suppressions endpoint to export all suppressed addresses via the API.
  • Postmark: Go to your Message Stream's Suppressions tab and click Export, or use the Suppressions API to pull a dump programmatically.

Once you have your list of addresses, save them to a plain text file with one email per line and follow the steps below.

The API

Each suppression is created individually via a POST request. For bulk imports you'll loop over your list and call the endpoint once per address.

POST /v2/accounts/{account_id}/suppressions

Required fields:

  • email - the address to suppress (must be a valid email address)
  • expires_at - when the suppression expires (RFC3339 format)

Optional fields:

  • domain - restrict the suppression to a specific sending domain. If omitted, the suppression applies to all domains on the account.
  • reason - a note on why the address was suppressed (max 255 characters)

If you're importing permanent suppressions (hard bounces, spam complaints), set expires_at far in the future - for example, 10 years from now. AhaSend requires an expiry date, but nothing stops you from setting it well beyond your product's expected lifetime.

Importing with cURL

For small lists or one-off imports, cURL is the quickest option:

curl -X POST https://api.ahasend.com/v2/accounts/YOUR_ACCOUNT_ID/suppressions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "expires_at": "2035-01-01T00:00:00Z",
    "reason": "Hard bounce"
  }'

For a larger list, use a plain text file with one email address per line (no headers, no commas):

[email protected]
[email protected]
[email protected]
#!/bin/bash
API_KEY="YOUR_API_KEY"
ACCOUNT_ID="YOUR_ACCOUNT_ID"
EXPIRES_AT="2035-01-01T00:00:00Z"

while IFS= read -r email; do
  [[ -z "$email" || "$email" == \#* ]] && continue

  response=$(curl -s -o /dev/null -w "%{http_code}" \
    -X POST "https://api.ahasend.com/v2/accounts/$ACCOUNT_ID/suppressions" \
    -H "Authorization: Bearer $API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"email\": \"$email\", \"expires_at\": \"$EXPIRES_AT\", \"reason\": \"Imported from previous ESP\"}")

  if [ "$response" == "201" ]; then
    echo "Suppressed: $email"
  elif [ "$response" == "409" ]; then
    echo "Already suppressed: $email"
  else
    echo "Failed ($response): $email"
  fi

done < emails.txt

A 409 response means the address is already suppressed - that's not an error, just skip it and move on. For CSV files with multiple columns, use the Python script below instead.

Importing with Python

For larger lists or when you need more control, Python with httpx or requests is a better fit. This example reads a CSV with email addresses and optional reasons, and imports them with basic rate limiting:

import csv
import time
import httpx

API_KEY = "YOUR_API_KEY"
ACCOUNT_ID = "YOUR_ACCOUNT_ID"
EXPIRES_AT = "2035-01-01T00:00:00Z"
RATE_LIMIT_DELAY = 0.05  # 50ms between requests = ~20 req/s

def import_suppressions(csv_path: str) -> None:
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    }

    with httpx.Client(timeout=10) as client:
        with open(csv_path, newline="") as f:
            reader = csv.DictReader(f)
            for row in reader:
                email = row.get("email", "").strip()
                reason = row.get("reason", "Imported from previous ESP").strip()

                if not email:
                    continue

                payload = {
                    "email": email,
                    "expires_at": EXPIRES_AT,
                    "reason": reason,
                }

                try:
                    response = client.post(
                        f"https://api.ahasend.com/v2/accounts/{ACCOUNT_ID}/suppressions",
                        headers=headers,
                        json=payload,
                    )

                    if response.status_code == 201:
                        print(f"Suppressed: {email}")
                    elif response.status_code == 409:
                        print(f"Already suppressed: {email}")
                    elif response.status_code == 429:
                        print("Rate limited - waiting 5 seconds...")
                        time.sleep(5)
                    else:
                        print(f"Failed ({response.status_code}): {email} - {response.text}")

                except httpx.RequestError as e:
                    print(f"Request error for {email}: {e}")

                time.sleep(RATE_LIMIT_DELAY)

if __name__ == "__main__":
    import_suppressions("suppressions.csv")

Expected CSV format:

email,reason
[email protected],Hard bounce
[email protected],Spam complaint
[email protected],Invalid address

Importing with the Go SDK

If you're already using the AhaSend Go SDK, you can integrate suppression imports directly into your codebase:

package main

import (
    "context"
    "encoding/csv"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/AhaSend/ahasend-go"
    "github.com/AhaSend/ahasend-go/api"
    "github.com/AhaSend/ahasend-go/models/requests"
    "github.com/google/uuid"
)

func main() {
    client := api.NewAPIClient(
        api.WithAPIKey(os.Getenv("AHASEND_API_KEY")),
    )

    accountID := uuid.MustParse(os.Getenv("AHASEND_ACCOUNT_ID"))
    expiresAt := time.Date(2035, 1, 1, 0, 0, 0, 0, time.UTC)

    f, err := os.Open("suppressions.csv")
    if err != nil {
        log.Fatalf("Failed to open CSV: %v", err)
    }
    defer f.Close()

    reader := csv.NewReader(f)
    records, err := reader.ReadAll()
    if err != nil {
        log.Fatalf("Failed to read CSV: %v", err)
    }

    // Skip header row
    for _, record := range records[1:] {
        if len(record) < 1 || record[0] == "" {
            continue
        }

        email := record[0]
        reason := "Imported from previous ESP"
        if len(record) > 1 && record[1] != "" {
            reason = record[1]
        }

        _, _, err := client.SuppressionsAPI.CreateSuppression(
            context.Background(),
            accountID,
            requests.CreateSuppressionRequest{
                Email:     email,
                Reason:    ahasend.String(reason),
                ExpiresAt: expiresAt,
            },
        )

        if err != nil {
            fmt.Printf("Failed to suppress %s: %v\n", email, err)
            continue
        }

        fmt.Printf("Suppressed: %s\n", email)
        time.Sleep(50 * time.Millisecond)
    }
}

Rate limits

The Suppressions API falls under AhaSend's general API rate limit. For large imports, add a short delay between requests (50ms gives you roughly 20 requests per second, which is safe). If you receive a 429 response, back off for a few seconds before retrying.

For very large lists (tens of thousands of addresses), consider running the import in batches overnight or outside peak hours.

Verifying the import

Once your import is complete, check your suppression list via the API:

curl "https://api.ahasend.com/v2/accounts/YOUR_ACCOUNT_ID/suppressions?limit=100" \
  -H "Authorization: Bearer YOUR_API_KEY"

The response includes pagination - use the next_cursor value to page through your full list if you have more than 100 suppressions.

Further reading

How to Import a Suppression List | AhaSend