Skip to main content

Official Python SDK for the Signal AI / AI Newsletter public REST API.

Project description

ai-newsletter

Official Python SDK for the Signal AI public REST API (/v1). Python 3.9+, built on httpx.


Install

pip install ai-newsletter
# or
uv pip install ai-newsletter
poetry add ai-newsletter

Requires Python 3.9+.


Get an API key

  1. Sign in at https://ai-newsletter.app.
  2. Open Settings → Public API (/settings/api).
  3. Click Create key, pick the scopes you need, copy the key — it is shown once.

Key prefixes:

Prefix Environment Notes
sk_live_… Production Hits real subscribers, real SES sends, real webhooks.
sk_test_… Sandbox Reads/writes parallel test tables. Sends never invoke SES.

Available scopes

Scope Allows
read:me Read account context (account.me)
read:subscribers List subscribers
write:subscribers Add subscribers, batch import, unsubscribe
send:newsletters Trigger transactional / broadcast sends
read:sends Read send job status
manage:webhooks CRUD webhook endpoints, list & replay deliveries
read:analytics Read newsletter and campaign analytics

Initialize the client

from ai_newsletter import AiNewsletter

with AiNewsletter(api_key="sk_live_…") as client:  # also accepts sk_test_…
    print(client.is_test)  # True when using an sk_test_ key
    me = client.account.me()

The client wraps an httpx.Client. Use it as a context manager (with …) or call client.close() manually when you are done.

Options

client = AiNewsletter(
    api_key="…",                                    # required
    base_url="https://…/functions/v1",              # override the default base URL
    timeout=30.0,                                   # per-request timeout (seconds)
    max_retries=3,                                  # retries for 408/425/429/5xx + network
)

Endpoint reference

All methods return the unwrapped data field of the response envelope as plain dicts/lists, and raise AiNewsletterError on non-2xx responses.

Account — client.account

account.me() -> dict

me = client.account.me()
# {"user_id": "...", "plan": "pro", "display_name": "...", "newsletters": [...]}

Subscribers — client.subscribers

list(*, newsletter_id, status=None, cursor=None, limit=None)

page = client.subscribers.list(
    newsletter_id="d5…",
    status="subscribed",  # "subscribed" | "unsubscribed" | "pending"
    limit=100,            # default 50, max 100
)
# {"items": [...], "next_cursor": "..." | None}

iterate(*, newsletter_id, status=None, limit=None) — generator

for sub in client.subscribers.iterate(newsletter_id="d5…"):
    print(sub["email"])

create(*, newsletter_id, email, name=None)

sub = client.subscribers.create(
    newsletter_id="d5…",
    email="jane@example.com",
    name="Jane",
)

batch(*, newsletter_id, subscribers)

Bulk-import up to 1,000 subscribers per call. Idempotent for 24h via the auto-generated Idempotency-Key.

result = client.subscribers.batch(
    newsletter_id="d5…",
    subscribers=[
        {"email": "a@example.com"},
        {"email": "b@example.com", "name": "B"},
    ],
)
# {"created": …, "skipped": …, "failed": …, "total": …, "results": [...]}

Per-row status is one of created / duplicate / suppressed / invalid.

unsubscribe(id)

Soft unsubscribe. The subscriber is never hard-deleted.

client.subscribers.unsubscribe("sub_…")

Sends — client.sends

create(**body)

Accepts newsletter_id, type ("transactional" or "broadcast"), and either to + subject + html/text, or a draft_id.

# Transactional
job = client.sends.create(
    newsletter_id="d5…",
    type="transactional",
    to="jane@example.com",
    subject="Welcome!",
    html="<p>Hi Jane</p>",
)

# Broadcast from an existing draft
client.sends.create(
    newsletter_id="d5…",
    type="broadcast",
    draft_id="draft_…",
)

retrieve(id)

job = client.sends.retrieve("snd_…")
# {"id", "status", "type", "newsletter_id", "recipient_count", "error", "created_at", "completed_at"}

list(*, cursor=None, limit=None) and iterate(*, limit=None)

for job in client.sends.iterate():
    print(job["id"], job["status"])

Webhooks — client.webhooks

list()

endpoints = client.webhooks.list()["items"]

create(*, url, event_types)

The response includes secret once — store it; it is used to verify incoming deliveries.

endpoint = client.webhooks.create(
    url="https://yourapp.com/webhooks/ai-newsletter",
    event_types=["send.completed", "subscriber.unsubscribed"],
)
print(endpoint["secret"])  # store securely

update(id, **body)url, event_types, is_active

delete(id)

deliveries(endpoint_id)

deliveries = client.webhooks.deliveries("whk_…")["items"]

replay(endpoint_id, delivery_id)

Clones the original payload into a new pending delivery.

client.webhooks.replay("whk_…", "whd_…")

Supported event types

Event Triggered when
send.queued A send job has been accepted
send.completed A send job finished successfully
send.failed A send job failed
subscriber.created A new subscriber was added
subscriber.unsubscribed A subscriber unsubscribed
newsletter.published A campaign was published

Analytics — client.analytics

Requires the read:analytics scope.

list_newsletters()

items = client.analytics.list_newsletters()["items"]

for_newsletter(id)

stats = client.analytics.for_newsletter("d5…")
print(stats["open_rate"], stats["click_rate"])

for_campaign(id)

stats = client.analytics.for_campaign("cmp_…")

Test keys return zeroed shapes so demos never read live data.


Pagination

List endpoints return:

{"items": [...], "next_cursor": "..." | None}

The cursor is opaque (base64 of iso_created_at|id). Pass it back as cursor= to fetch the next page, or use iterate() to let the SDK do it for you.


Idempotency

Every non-GET request automatically includes an Idempotency-Key header. Replays within 24h return the original result, so retries during network blips are safe.

subscribers.batch is the most useful target — re-running the same job will not double-import rows.


Retries & timeouts

The SDK retries 408, 425, 429, 500, 502, 503, 504, and any network error using exponential backoff with jitter. Retry-After (when present) is honoured.

client = AiNewsletter(api_key="…", max_retries=5, timeout=60.0)

Errors

from ai_newsletter import AiNewsletterError

try:
    client.subscribers.create(newsletter_id="d5…", email="bad")
except AiNewsletterError as e:
    print(e.status, e.code, str(e), e.retry_after, e.body)
code HTTP Meaning
missing_api_key 401 No Authorization header
invalid_api_key 401 Key not recognized
revoked_api_key 401 Key has been revoked
scope_denied 403 Key lacks the required scope
rate_limited 429 Rate limit or auto-throttle hit. See Retry-After
invalid_request 422 Body or query failed validation
not_found 404 Resource does not exist or is not yours
conflict 409 Idempotency-Key reused with a different payload
internal_error 500 Unexpected server error — safe to retry

Verify webhooks

Header format: X-Webhook-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256>.

from flask import Flask, request, abort
from ai_newsletter import verify_webhook_signature

app = Flask(__name__)
SECRET = "whsec_…"

@app.post("/webhooks/ai-newsletter")
def webhook():
    sig = request.headers.get("X-Webhook-Signature", "")
    if not verify_webhook_signature(request.get_data(as_text=True), sig, SECRET):
        abort(401)
    event = request.get_json()
    # handle event["type"] / event["data"]
    return "", 200

verify_webhook_signature(raw_body, header, secret, tolerance_seconds=300) rejects timestamps outside the tolerance window to prevent replay.


Rate limits

Plan Burst / sustained
Free / Starter 60 req/min
Pro 300 req/min
Business 1,000 req/min

Response headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After (on 429). Auto-throttling temporarily blocks keys with abnormal error rates — see your dashboard for status.


Test mode

Pass an sk_test_… key — every call hits the parallel sandbox tables, sends short-circuit SES, and webhooks fire to test endpoints only. client.is_test reports the mode.


Self-hosting

Override the base URL if you run a fork of the API:

client = AiNewsletter(
    api_key="…",
    base_url="https://your-fork.example.com/functions/v1",
)

License

MIT — see LICENSE and CHANGELOG.md.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ai_newsletter-1.0.3.tar.gz (12.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ai_newsletter-1.0.3-py3-none-any.whl (8.0 kB view details)

Uploaded Python 3

File details

Details for the file ai_newsletter-1.0.3.tar.gz.

File metadata

  • Download URL: ai_newsletter-1.0.3.tar.gz
  • Upload date:
  • Size: 12.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for ai_newsletter-1.0.3.tar.gz
Algorithm Hash digest
SHA256 6c93602f62091bc84ae545b6295d401b9f07dc98810d33db189797c48a4e8251
MD5 7d621296738f821514341b96a2b08649
BLAKE2b-256 26d07d3feb5c4ab4329e2227d8533b5b58f3eb88940f20dc201cec2cde0817dc

See more details on using hashes here.

File details

Details for the file ai_newsletter-1.0.3-py3-none-any.whl.

File metadata

  • Download URL: ai_newsletter-1.0.3-py3-none-any.whl
  • Upload date:
  • Size: 8.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for ai_newsletter-1.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 914a0f79167db7b2bc00b6be81db6cbea066dc8ec36a2c010dac48450daf8efa
MD5 d78251e4fbeb7f24280222d2ab51bdef
BLAKE2b-256 49917e5afb4e6b8cdf761b162d9fc37cdde7ceff47168e22464f889c1ad29f4f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page