Skip to main content

PyLoops

Unofficial Python SDK for Loops.so.

PyPI version Python 3.12+

Installation

pip install pyloops

Or with uv:

uv add pyloops

Quick Start

PyLoops offers two ways to interact with the Loops API:

High-Level API

import pyloops

# Configure once (or set LOOPS_API_KEY environment variable)
pyloops.configure(api_key="your_api_key_here")

# Get the client
client = pyloops.get_client()

# Upsert a contact
await client.upsert_contact(
    email="user@example.com",
    first_name="John",
    last_name="Doe",
    subscribed=True,
)

# Find a contact
contacts = await client.find_contact(email="user@example.com")

# Send an event
await client.send_event(
    event_name="user_signup",
    email="user@example.com",
    event_properties={"plan": "premium"},
)

# List mailing lists
mailing_lists = await client.list_mailing_lists()

# Look up the data variables a transactional template expects, then send it
variables = await client.get_transactional_variables("clfq6dinn000yl70fgwwyp82l")
# -> ["firstName", "inviteLink"]

await client.send_transactional_email(
    transactional_id="clfq6dinn000yl70fgwwyp82l",
    email="user@example.com",
    data_variables={
        "firstName": "Jane",
        "inviteLink": "https://myapp.com/invite/abc123",
    },
)

Low-Level API

For more control, use the auto-generated low-level API directly:

from pyloops import AuthenticatedClient
from pyloops._generated.api.contacts import put_v1_contacts_update
from pyloops._generated.models import ContactUpdateRequest

# Note: the base URL must NOT include the "/v1" segment — it is now part of
# each endpoint path (e.g. /v1/contacts/update).
client = AuthenticatedClient(
    base_url="https://app.loops.so/api",
    token="your_api_key_here",
)

response = await put_v1_contacts_update.asyncio(
    client=client,
    body=ContactUpdateRequest(
        email="user@example.com",
        first_name="John",
        last_name="Doe"
    )
)

Authentication

All API calls require a Loops API key. Get your API key from your Loops account settings.

There are three ways to configure authentication:

  1. Environment variable:
export LOOPS_API_KEY="your_api_key_here"
  1. Module-level configuration:
import pyloops
pyloops.configure(api_key="your_api_key_here")
  1. Per-client configuration:
import pyloops
client = pyloops.LoopsClient(api_key="your_api_key_here")

Features

The high-level LoopsClient wraps every Loops.so API endpoint:

  • Contacts: Create, upsert, find, and delete contacts
  • Contact Properties: List and create custom contact properties
  • Contact Suppression: Read and remove suppression status
  • Mailing Lists: View available mailing lists
  • Events: Trigger event-based emails
  • Event Patterns: List and look up triggerable event types
  • Transactional Emails: Send, list, and manage transactional templates (create, update, draft, publish), and read the data variables a template expects
  • Campaigns & Campaign Groups: List, get, create, and update
  • Transactional Groups: List, get, create, and update
  • Themes & Components: List, get, create, and update reusable branding/building blocks
  • Email Messages: Get, update, preview, and run Guardian content checks
  • Workflows & Workflow Nodes: Create and update workflows, change mailing lists, and build the node graph (create/update/branch/reroute/delete nodes)
  • Audience Segments: List, get, and create
  • Uploads: Create and complete asset uploads
  • Sending IPs: Retrieve dedicated sending IP addresses

Safe Mode

When developing locally, you can enable safe mode to prevent accidentally sending emails or syncing contacts to real addresses. With safe mode enabled, only emails matching your allowed domains will be accepted — all others will raise a LoopsUnsafeEmailError.

import pyloops

pyloops.configure(
    api_key="your_api_key_here",
    safe_mode=True,
    safe_mode_allowed_domains=("@test.com", "@example.com", "@yourcompany.com"),
)

client = pyloops.get_client()

# This works
await client.send_transactional_email(
    transactional_id="welcome",
    email="dev@test.com",
)

# This raises LoopsUnsafeEmailError
await client.send_transactional_email(
    transactional_id="welcome",
    email="real-user@gmail.com",
)

You can also set it per-client:

client = pyloops.LoopsClient(
    api_key="your_api_key_here",
    safe_mode=True,
    safe_mode_allowed_domains=("@test.com",),
)

Testing

PyLoops ships a testing module that mocks all Loops API endpoints at the HTTP transport level using respx. Your real client code runs end-to-end, but no actual API requests leave the process.

Install with the testing extra:

pip install pyloops[testing]

Basic usage

import json
import pyloops
from pyloops.testing import loops_respx_mock

async def test_sends_welcome_email():
    with loops_respx_mock() as api:
        client = pyloops.get_client()
        await client.send_transactional_email(
            transactional_id="welcome",
            email="user@test.com",
            data_variables={"name": "Jan"},
        )

        # Inspect the HTTP request that pyloops made
        request = api["transactional"].calls[0].request
        body = json.loads(request.content)
        assert body["transactionalId"] == "welcome"
        assert body["email"] == "user@test.com"

Pytest fixture

import pytest
from pyloops.testing import loops_respx_mock

@pytest.fixture
def loops_api():
    with loops_respx_mock() as router:
        yield router

async def test_create_contact(loops_api):
    client = pyloops.get_client()
    result = await client.create_contact(email="new@test.com")
    assert result.success is True
    assert loops_api["create_contact"].called

Simulating errors

Override any route to return custom responses:

from httpx import Response

async def test_handles_rate_limit(loops_api):
    loops_api["transactional"].mock(
        return_value=Response(
            429,
            json={"success": False},
            headers={"x-ratelimit-limit": "10", "x-ratelimit-remaining": "0"},
        )
    )
    client = pyloops.get_client()
    with pytest.raises(pyloops.LoopsRateLimitError):
        await client.send_transactional_email(transactional_id="abc", email="user@test.com")

Available mock routes

Every Loops endpoint the client supports has a matching mock route, accessible by name on the yielded router. Route names mirror the client method names, so a call to client.create_workflow(...) is recorded under api["create_workflow"]. Common routes:

Name Method Endpoint
health GET /api-key
transactional POST /transactional
list_transactional GET /transactional
create_contact POST /contacts/create
upsert_contact PUT /contacts/update
find_contact GET /contacts/find
delete_contact POST /contacts/delete
list_contact_properties GET /contacts/properties
create_contact_property POST /contacts/properties
send_event POST /events/send
list_mailing_lists GET /lists
list_sending_ips GET /dedicated-sending-ips

Additional families are mocked too — campaigns, campaign/transactional groups, transactional templates, themes, components, email messages (incl. preview and Guardian), uploads, audience segments, event patterns, and workflows/workflow nodes. See EXPECTED_ROUTE_NAMES in tests/test_testing.py for the full list.

Testing with safe mode

The mock disables safe mode by default. To test safe mode behavior, pass safe_mode=True and your allowed domains:

with loops_respx_mock(safe_mode=True, safe_mode_allowed_domains=("@test.com",)) as api:
    client = pyloops.get_client()
    await client.send_transactional_email(transactional_id="t1", email="dev@test.com")  # OK
    await client.send_transactional_email(transactional_id="t1", email="user@gmail.com")  # raises

Documentation

For detailed API documentation, visit the Loops.so API docs.

Automated Updates

This SDK is automatically updated to match the latest Loops.so API specification. The package version corresponds to the Loops API version (current: 1.21.7.1). A three-segment version (e.g. 1.21.2) tracks the Loops API version directly; a fourth segment (e.g. 1.21.2.1) denotes a client-wrapper update built on top of that API version.

A GitHub Action checks for API updates daily and creates a pull request when changes are detected. After review and merge, a new version is automatically published to PyPI.

Development

Setup

# Clone the repository
git clone https://github.com/doctorgpt-corp/pyloops.git
cd pyloops

# Install dependencies with uv
uv sync --all-groups

Running Tests

Using just:

just check      # Run linting + type checking
just lint       # Run linting only
just typecheck  # Run type checking only
just fmt        # Format code

Or directly with uv:

uv run ruff check src/
uv run pyright src/

Project Structure

src/pyloops/
├── __init__.py          # Main exports
├── client.py            # High-level LoopsClient wrapper
├── config.py            # Configuration
├── exceptions.py        # Exceptions
├── api/                 # Re-exports from _generated.api
├── models/              # Re-exports from _generated.models
└── _generated/          # ALL auto-generated code
    ├── client.py
    ├── api/
    ├── models/
    └── types.py

Regenerate SDK

To manually regenerate the SDK from the latest OpenAPI spec:

just generate

Or manually:

rm -rf src/pyloops/_generated
uv tool run openapi-python-client generate --url https://app.loops.so/openapi.yaml --meta uv
mv loops-open-api-spec-client/loops_open_api_spec_client src/pyloops/_generated
rm -rf loops-open-api-spec-client openapi.yaml

Custom code is never touched during regeneration.

License

MIT

Disclaimer

This is an unofficial SDK and is not affiliated with or endorsed by Loops.so.

Download files

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

Source Distribution

pyloops-1.21.7.1.tar.gz (120.1 kB view details)

Uploaded Source

Built Distribution

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

pyloops-1.21.7.1-py3-none-any.whl (375.4 kB view details)

Uploaded Python 3

File details

Details for the file pyloops-1.21.7.1.tar.gz.

File metadata

  • Download URL: pyloops-1.21.7.1.tar.gz
  • Upload date:
  • Size: 120.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pyloops-1.21.7.1.tar.gz
Algorithm Hash digest
SHA256 6ec0a5d872916ea057dad4fe770086b5e051b6b0d630687938d840284b7c3596
MD5 97dabe2ae57e41b7f1b86b69c2a992c5
BLAKE2b-256 cb329d8b35722d8701242ab60dca54d86300ea9241f4deb276f5fa96fe1d3a1a

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyloops-1.21.7.1.tar.gz:

Publisher: publish.yml on doctorgpt-corp/pyloops

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyloops-1.21.7.1-py3-none-any.whl.

File metadata

  • Download URL: pyloops-1.21.7.1-py3-none-any.whl
  • Upload date:
  • Size: 375.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for pyloops-1.21.7.1-py3-none-any.whl
Algorithm Hash digest
SHA256 94c77d28a26df610a0f92aec5fbd8e1af122d5532bd1d4d7378ee8910ef2a18b
MD5 1c57676ab4ee9affccd78ed889b73bda
BLAKE2b-256 c69c3c8e614cad215e6634054d1b3ce5eaea6fb86b0b4aa811b6034fb0ea18cb

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyloops-1.21.7.1-py3-none-any.whl:

Publisher: publish.yml on doctorgpt-corp/pyloops

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.21.7.1 This release

2 files

1.21.7

2 files

1.21.2.1

2 files

1.14.2

2 files

1.8.0

2 files

1.7.0.1

2 files

1.7.0

2 files

1.6.1.4

2 files

1.6.1.3

2 files

1.6.1.2

2 files

1.6.1.1

2 files

1.6.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page