Skip to main content
Requirements: Python 3.9 or higher

Installation

pip install ark-email
For async support with aiohttp (recommended for high-throughput applications):
pip install ark-email[aiohttp]

Quick Start

import os
from ark import Ark

# Initialize the client
client = Ark(api_key=os.environ.get("ARK_API_KEY"))

# Send an email
email = client.emails.send(
    from_="Security <[email protected]>",
    to=["[email protected]"],
    subject="Reset your password",
    html="<h1>Password Reset</h1><p>Click the link below to reset your password.</p>",
    text="Password Reset\n\nClick the link below to reset your password."
)

print(f"Email ID: {email.data.id}")
print(f"Status: {email.data.status}")
The from_ parameter uses an underscore because from is a reserved keyword in Python.

Configuration

Client Options

from ark import Ark

client = Ark(
    api_key="ark_...",           # Required (or set ARK_API_KEY env var)
    timeout=60.0,                 # Request timeout in seconds (default: 60)
    max_retries=2,                # Number of retry attempts (default: 2)
    base_url="https://api.arkhq.io/v1",  # API base URL (rarely needed)
)

Environment Variables

export ARK_API_KEY="ark_live_..."
export ARK_LOG="debug"  # Enable debug logging

API Reference

Emails

Send a single email.
email = client.emails.send(
    from_="[email protected]",
    to=["[email protected]"],
    subject="Welcome!",
    html="<h1>Hello</h1>",
    text="Hello",  # Optional but recommended
    cc=["[email protected]"],  # Optional
    bcc=["[email protected]"],  # Optional
    reply_to="[email protected]",  # Optional
    tags=["welcome", "onboarding"],  # Optional
    metadata={"user_id": "123"},  # Optional
    track_opens=True,  # Optional
    track_clicks=True,  # Optional
    scheduled_at="2024-01-20T09:00:00Z",  # Optional - ISO 8601
    attachments=[  # Optional
        {
            "filename": "invoice.pdf",
            "content": base64_encoded_content,
            "content_type": "application/pdf"
        }
    ],
    headers={  # Optional custom headers
        "X-Custom-Header": "value"
    }
)
Returns: SendEmailResponse with data.id, data.status, etc.
Send multiple emails in a single request.
result = client.emails.send_batch(
    emails=[
        {
            "from_": "[email protected]",
            "to": ["[email protected]"],
            "subject": "Hello User 1",
            "html": "<p>Hello!</p>"
        },
        {
            "from_": "[email protected]",
            "to": ["[email protected]"],
            "subject": "Hello User 2",
            "html": "<p>Hello!</p>"
        }
    ]
)

for email in result.data:
    print(f"{email.id}: {email.status}")
Send a raw MIME message.
raw_message = """From: [email protected]
To: [email protected]
Subject: Raw email
Content-Type: text/plain

This is a raw MIME message."""

email = client.emails.send_raw(
    from_="[email protected]",
    to=["[email protected]"],
    raw_message=raw_message
)
List emails with filtering and pagination.
# List recent emails
emails = client.emails.list(
    page=1,
    per_page=25,
    status="sent",      # Optional filter
    tag="welcome",      # Optional filter
)

for email in emails.data:
    print(f"{email.id}: {email.subject} - {email.status}")

# Pagination info
print(f"Page {emails.page} of {emails.total_pages}")
Get a single email by ID.
email = client.emails.retrieve("msg_abc123xyz")

print(f"Subject: {email.data.subject}")
print(f"Status: {email.data.status}")
print(f"Created: {email.data.created_at}")
Get delivery attempts for an email.
deliveries = client.emails.get_deliveries("msg_abc123xyz")

for delivery in deliveries.data:
    print(f"Attempt at {delivery.timestamp}: {delivery.status}")
    if delivery.error:
        print(f"  Error: {delivery.error}")
Retry a failed email.
result = client.emails.retry("msg_abc123xyz")
print(f"Retry scheduled: {result.data.id}")

Domains

Register a new sending domain.
domain = client.domains.create(name="mail.yourdomain.com")

print(f"Domain ID: {domain.data.id}")
print("DNS Records to configure:")
for record in domain.data.dns_records:
    print(f"  {record.type} {record.name} -> {record.value}")
List all domains.
domains = client.domains.list()

for domain in domains.data:
    status = "Verified" if domain.verified else "Pending"
    print(f"{domain.name}: {status}")
Get domain details.
domain = client.domains.retrieve("dom_abc123")
print(f"Domain: {domain.data.name}")
print(f"Verified: {domain.data.verified}")
Trigger DNS verification.
result = client.domains.verify("dom_abc123")

if result.data.verified:
    print("Domain verified successfully!")
else:
    print("DNS records not found. Please check your configuration.")
Remove a domain.
client.domains.delete("dom_abc123")
print("Domain deleted")

Suppressions

Add an email to the suppression list.
suppression = client.suppressions.create(
    email="[email protected]",
    reason="hard_bounce"
)
Add multiple emails to the suppression list.
result = client.suppressions.bulk_create(
    suppressions=[
        {"email": "[email protected]", "reason": "hard_bounce"},
        {"email": "[email protected]", "reason": "complaint"},
    ]
)
print(f"Added {result.data.created_count} suppressions")
List suppressed emails.
suppressions = client.suppressions.list(
    page=1,
    per_page=50,
    reason="hard_bounce"  # Optional filter
)

for s in suppressions.data:
    print(f"{s.email}: {s.reason} (added {s.created_at})")
Check if an email is suppressed.
try:
    suppression = client.suppressions.retrieve("[email protected]")
    print(f"Suppressed: {suppression.data.reason}")
except ark.NotFoundError:
    print("Email is not suppressed")
Remove from suppression list.
client.suppressions.delete("[email protected]")
print("Removed from suppression list")

Webhooks

Create a webhook endpoint.
webhook = client.webhooks.create(
    url="https://yourapp.com/webhooks/ark",
    events=["MessageSent", "MessageBounced", "MessageLoaded"]
)

print(f"Webhook ID: {webhook.data.id}")
print(f"Signing Secret: {webhook.data.signing_secret}")  # Store this!
List all webhooks.
webhooks = client.webhooks.list()

for wh in webhooks.data:
    print(f"{wh.id}: {wh.url}")
    print(f"  Events: {', '.join(wh.events)}")
Update webhook configuration.
webhook = client.webhooks.update(
    "wh_abc123",
    events=["MessageSent", "MessageBounced"]  # Updated events
)
Send a test event to your webhook.
result = client.webhooks.test(
    "wh_abc123",
    event="MessageSent"
)
print(f"Test result: {result.data.status}")
Delete a webhook.
client.webhooks.delete("wh_abc123")

Tracking

Configure a custom tracking domain.
tracking = client.tracking.create(
    domain="track.yourdomain.com"
)

print("Add this CNAME record:")
print(f"  {tracking.data.cname_target}")
List tracking domains.
tracking_domains = client.tracking.list()

for t in tracking_domains.data:
    status = "Verified" if t.verified else "Pending"
    print(f"{t.domain}: {status}")
Verify tracking domain DNS.
result = client.tracking.verify("trk_abc123")
print(f"Verified: {result.data.verified}")
Remove a tracking domain.
client.tracking.delete("trk_abc123")

Async Usage

For high-throughput applications, use the async client:
import asyncio
from ark import AsyncArk

async def send_welcome_emails(user_emails: list[str]):
    client = AsyncArk()

    # Send emails concurrently
    tasks = [
        client.emails.send(
            from_="[email protected]",
            to=[email],
            subject="Welcome to MyApp!",
            html="<h1>Welcome!</h1>"
        )
        for email in user_emails
    ]

    results = await asyncio.gather(*tasks)

    for result in results:
        print(f"Sent to {result.data.to[0]}: {result.data.id}")

# Run it
asyncio.run(send_welcome_emails([
    "[email protected]",
    "[email protected]",
    "[email protected]"
]))

Context Manager

Both sync and async clients support context managers for proper resource cleanup:
# Sync
with Ark() as client:
    client.emails.send(...)

# Async
async with AsyncArk() as client:
    await client.emails.send(...)

Error Handling

The SDK throws typed exceptions for different error scenarios:
from ark import Ark
import ark

client = Ark()

try:
    email = client.emails.send(
        from_="[email protected]",
        to=["invalid"],
        subject="Test",
        html="<p>Test</p>"
    )
except ark.BadRequestError as e:
    print(f"Invalid request: {e.message}")
    print(f"Error code: {e.code}")
except ark.AuthenticationError:
    print("Invalid API key")
except ark.RateLimitError as e:
    print(f"Rate limited. Retry after: {e.response.headers.get('Retry-After')}")
except ark.APIConnectionError:
    print("Network error - check your connection")
except ark.APIStatusError as e:
    print(f"API error {e.status_code}: {e.message}")

Exception Hierarchy

ark.APIError
├── ark.APIConnectionError      # Network/connection issues
├── ark.APITimeoutError         # Request timeout
└── ark.APIStatusError          # HTTP error responses
    ├── ark.BadRequestError         # 400
    ├── ark.AuthenticationError     # 401
    ├── ark.PermissionDeniedError   # 403
    ├── ark.NotFoundError           # 404
    ├── ark.UnprocessableEntityError # 422
    ├── ark.RateLimitError          # 429
    └── ark.InternalServerError     # 5xx

Advanced Usage

Raw Response Access

Access HTTP headers and status codes:
response = client.emails.with_raw_response.send(
    from_="[email protected]",
    to=["[email protected]"],
    subject="Test",
    html="<p>Test</p>"
)

print(f"Status: {response.status_code}")
print(f"Headers: {response.headers}")
print(f"Request ID: {response.headers.get('x-request-id')}")

# Parse the response body
email = response.parse()
print(f"Email ID: {email.data.id}")

Streaming Responses

For large responses, use streaming to reduce memory usage:
with client.emails.with_streaming_response.list() as response:
    for chunk in response.iter_bytes():
        process(chunk)

Custom HTTP Client

Use a custom httpx client for proxies or advanced configuration:
import httpx
from ark import Ark

# Configure proxy
http_client = httpx.Client(
    proxies="http://proxy.example.com:8080",
    verify=False,  # Skip SSL verification (not recommended for production)
)

client = Ark(http_client=http_client)

Idempotency

Use idempotency keys to safely retry requests:
import uuid

idempotency_key = str(uuid.uuid4())

# This request can be safely retried with the same key
email = client.emails.send(
    from_="[email protected]",
    to=["[email protected]"],
    subject="Order Confirmation",
    html="<p>Your order is confirmed.</p>",
    idempotency_key=idempotency_key
)

Accessing Undocumented Fields

The SDK models allow access to undocumented API fields:
email = client.emails.retrieve("msg_abc123")

# Access known fields
print(email.data.subject)

# Access any additional fields returned by the API
print(email.data.model_extra)  # Dict of extra fields

Type Hints

The SDK is fully typed. Your IDE will provide autocomplete and type checking:
from ark import Ark
from ark.types import EmailSendParams

def send_email(params: EmailSendParams) -> str:
    client = Ark()
    response = client.emails.send(**params)
    return response.data.id

Framework Integration

Django

# settings.py
ARK_API_KEY = os.environ.get("ARK_API_KEY")

# emails.py
from django.conf import settings
from ark import Ark

_client = None

def get_ark_client():
    global _client
    if _client is None:
        _client = Ark(api_key=settings.ARK_API_KEY)
    return _client

def send_welcome_email(user):
    client = get_ark_client()
    return client.emails.send(
        from_="[email protected]",
        to=[user.email],
        subject=f"Welcome, {user.first_name}!",
        html=render_to_string("emails/welcome.html", {"user": user})
    )

FastAPI

from fastapi import FastAPI, Depends
from ark import AsyncArk
from contextlib import asynccontextmanager

app = FastAPI()
ark_client: AsyncArk | None = None

@asynccontextmanager
async def lifespan(app: FastAPI):
    global ark_client
    ark_client = AsyncArk()
    yield
    await ark_client.close()

app = FastAPI(lifespan=lifespan)

@app.post("/send-email")
async def send_email(to: str, subject: str, body: str):
    email = await ark_client.emails.send(
        from_="[email protected]",
        to=[to],
        subject=subject,
        html=body
    )
    return {"email_id": email.data.id}

Flask

from flask import Flask, g
from ark import Ark

app = Flask(__name__)

def get_ark():
    if 'ark' not in g:
        g.ark = Ark()
    return g.ark

@app.teardown_appcontext
def close_ark(error):
    ark = g.pop('ark', None)
    if ark is not None:
        ark.close()

@app.route("/send", methods=["POST"])
def send_email():
    client = get_ark()
    email = client.emails.send(...)
    return {"email_id": email.data.id}

Resources