Lightweight Python SDK (unofficial) for the Loops API
Project description
Unofficial Loops.so Python Library (pyloops-so)
pyloops-so is a lightweight Python SDK for the Loops API, designed for production usage with minimal dependencies.
Why this library
- Complete support for Loops.so endpoints
- Typed request/response models via Pydantic
- Optional raw JSON mode when you want plain dictionaries
- Lightweight runtime dependencies (
pydantic,requests) - Small, composable client structure (
contacts,events,transactional, etc.)
Loops API docs
Official Loops API reference: https://loops.so/docs/api-reference
Endpoint docs covered by this SDK:
- Create Contact
- Update Contact
- Find Contact
- Delete Contact
- Create Contact Property
- List Contact Properties
- List Mailing Lists
- Send Event
- Send Transactional Email
- List Transactional Emails
- API Key
- Dedicated Sending IPs
Installation
Install from PyPI:
uv add pyloops-so
For local development:
uv sync --extra dev
Authentication
Loops uses Bearer auth for all endpoints:
Authorization: Bearer {api_key}
Create a client once and reuse it:
from loops_py import LoopsClient
client = LoopsClient(api_key="loops_api_key")
If your network/provider applies strict bot filtering, set an explicit user agent:
client = LoopsClient(api_key="loops_api_key", user_agent="my-app/1.0")
Usage model
The SDK supports two call styles:
- Top-level convenience methods (
client.create_contact(...)) for compatibility. - Grouped service methods (
client.contacts.create_contact(...)) for clearer organization.
Both call styles use the same underlying implementation.
Typed mode (default)
By default, responses are returned as Pydantic models.
from loops_py import ContactRequest, LoopsClient
client = LoopsClient(api_key="loops_api_key")
created = client.contacts.create_contact(
ContactRequest(
email="ada@example.com",
first_name="Ada",
user_id="usr_123",
mailing_lists={"cll2pyfrx0000mm080fwnwdg0": True},
)
)
print(created.success)
print(created.id)
JSON mode
If you prefer raw dict/list responses, use response_mode="json".
from loops_py import LoopsClient
client = LoopsClient(api_key="loops_api_key", response_mode="json")
raw = client.account.verify_api_key()
print(raw["teamName"])
You can override per call:
typed = client.account.verify_api_key(as_json=False)
raw = client.account.verify_api_key(as_json=True)
Error handling
HTTP errors from Loops raise LoopsAPIError with status code and parsed response payload.
from loops_py import LoopsAPIError
try:
client.contacts.find_contact({"email": "missing@example.com"})
except LoopsAPIError as exc:
print(exc.status_code)
print(exc.response)
Rate limit handling and retries
Loops applies request rate limits (baseline 10 requests/second/team) and can return 429.
This SDK retries 429 responses automatically with exponential backoff.
Default retry behavior:
max_retries=3(up to 4 total attempts)retry_backoff_base=0.25secondsretry_backoff_max=4.0secondsretry_jitter=0.1(10% random jitter)Retry-Afterheader is honored when present
Configure it:
from loops_py import LoopsClient
client = LoopsClient(
api_key="loops_api_key",
max_retries=5,
retry_backoff_base=0.2,
retry_backoff_max=6.0,
retry_jitter=0.2,
)
Disable retries by setting max_retries=0.
Endpoint mapping
contactscreate_contact->POST /contacts/createupdate_contact->POST /contacts/updatefind_contact->GET /contacts/finddelete_contact->POST /contacts/deletecreate_contact_property->POST /contacts/propertieslist_contact_properties->GET /contacts/properties
mailing_listslist_mailing_lists->GET /lists
eventssend_event->POST /events/send
transactionalsend_transactional_email->POST /transactionallist_transactional_emails->GET /transactional
accountverify_api_key->GET /api-keylist_dedicated_sending_ips->GET /dedicated-sending-ips
Idempotency support
For endpoints that support idempotency, you can optionally pass idempotency_key:
client.events.send_event(
{"email": "user@example.com", "eventName": "signup"},
idempotency_key="signup-user@example.com-2026-02-28",
)
Build and publish
Build sdist + wheel:
uv build
Artifacts:
dist/*.tar.gzdist/*.whl
Publish (requires PyPI token):
export UV_PUBLISH_TOKEN="pypi-..."
uv publish
TestPyPI:
uv publish --publish-url https://test.pypi.org/legacy/
Development
uv run ruff check .
uv run pytest
License
MIT
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyloops_so-0.1.3.tar.gz.
File metadata
- Download URL: pyloops_so-0.1.3.tar.gz
- Upload date:
- Size: 47.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e2e6ccba0ae394dae3f226d83e3c7073be702679dc95317dc97bb4d741998de0
|
|
| MD5 |
446d8ac3ceb2c6fc56393f037936d1a1
|
|
| BLAKE2b-256 |
fa9f3936cbfb753418f620851674a6c882addee196493b7b7b4aa363844b39b7
|
Provenance
The following attestation bundles were made for pyloops_so-0.1.3.tar.gz:
Publisher:
publish.yml on annjawn/loops-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyloops_so-0.1.3.tar.gz -
Subject digest:
e2e6ccba0ae394dae3f226d83e3c7073be702679dc95317dc97bb4d741998de0 - Sigstore transparency entry: 1004617247
- Sigstore integration time:
-
Permalink:
annjawn/loops-py@7153982a0d5f9412cb0154b834578614ef97b61e -
Branch / Tag:
refs/tags/v0.1.3 - Owner: https://github.com/annjawn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7153982a0d5f9412cb0154b834578614ef97b61e -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyloops_so-0.1.3-py3-none-any.whl.
File metadata
- Download URL: pyloops_so-0.1.3-py3-none-any.whl
- Upload date:
- Size: 14.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
08be5d8728ee8d73e70439712b3010834cfdfc6d36b567aba104d46c92376c0b
|
|
| MD5 |
8a23b432211d3385ec794be19667eeda
|
|
| BLAKE2b-256 |
4d11c358d0921cf5a68e12d91b7a5effd89f39c1a4448216ef9dbf93eba55ebb
|
Provenance
The following attestation bundles were made for pyloops_so-0.1.3-py3-none-any.whl:
Publisher:
publish.yml on annjawn/loops-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyloops_so-0.1.3-py3-none-any.whl -
Subject digest:
08be5d8728ee8d73e70439712b3010834cfdfc6d36b567aba104d46c92376c0b - Sigstore transparency entry: 1004617248
- Sigstore integration time:
-
Permalink:
annjawn/loops-py@7153982a0d5f9412cb0154b834578614ef97b61e -
Branch / Tag:
refs/tags/v0.1.3 - Owner: https://github.com/annjawn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7153982a0d5f9412cb0154b834578614ef97b61e -
Trigger Event:
push
-
Statement type: