Skip to main content

political-comms

Python SDK for the Political Comms REST API. Direct-to-carrier political texting for campaigns, PACs, advocacy organizations, fundraisers, and elected officials.

Synchronous client built on httpx. Python 3.10 or later.

The full API reference lives at docs.politicalcomms.com and the OpenAPI 3.1 specification at politicalcomms.com/openapi.json.

Install

pip install political-comms

Authentication

Requests authenticate with an API key in the X-API-Key header. Keys are created in the dashboard under Admin > API and are prefixed pc_live_.

Set the key in the environment:

export POLITICAL_COMMS_API_KEY=pc_live_...
from political_comms import PoliticalCommsClient

client = PoliticalCommsClient()

Or pass it to the constructor:

client = PoliticalCommsClient(api_key="pc_live_...")

Quickstart

Verify the credential, then run the standard send workflow: create a project, send yourself a test, and schedule it.

from political_comms import PoliticalCommsClient

client = PoliticalCommsClient()

# 1. Verify the credential.
orgs = client.list_organizations()
print([org.get("display_name") for org in orgs["data"]])

# 2. Create a project.
created = client.create_project(
    organization_id="org_...",
    name="GOTV reminder",
    protocol="sms",
    message_text="Polls are open until 8pm. Find your polling place: {link}",
    phone_number_ids=["pn_..."],
    contact_list_ids=["cl_..."],
    brand_id="brand_...",
    campaign_id="camp_...",
)
project_id = created["data"]["id"]

# 3. Send a test to yourself.
client.test_project(project_id, [{"phone": "+15555550100"}])

# 4. Schedule the send.
client.schedule_project(project_id, "2026-11-03T09:00:00", "America/New_York")
# Optional: run the whole project past the brand's daily T-Mobile carrier limit
# instead of pausing at it each day. Over-limit messages to T-Mobile recipients
# may fail and are still billed.
# client.schedule_project(
#     project_id, "2026-11-03T09:00:00", "America/New_York", daily_cap_bypass=True
# )

One method exists per API operation, in snake_case: list_organizations, get_hierarchy, list_brands, list_campaigns, get_campaign_throughput, list_tracking_domains, list_phone_numbers, list_toll_free_verifications, get_toll_free_verification, list_contact_lists, get_contact_list, import_contact_list, analyze_contact_list, download_contact_list, delete_contact_list, list_media, import_media, get_media, delete_media, list_projects, create_project, get_all_project_stats, get_project, update_project, get_project_stats, get_project_throughput, test_project, schedule_project, unschedule_project, pause_project, copy_project, list_conversations, get_conversation, list_conversation_messages, reply_to_conversation, get_message_stats, get_ledger_usage, get_ledger_usage_by_initiator.

Every method returns the parsed JSON response, a dict of the form {"success": True, "data": ...}.

Conversations

A conversation is one thread between one of your sending numbers and one contact, created by a project send. The API never creates a conversation; it replies inside an existing one, from the same number, on the same project. list_conversations and list_conversation_messages are keyset paginated like the email lists.

# Reply to an inbound message.replied webhook.
client.reply_to_conversation(
    conversation_id, "Thanks for reaching out!", idempotency_key=f"reply-{message_id}"
)

# Recover inbound messages missed while a webhook endpoint was down.
page = client.list_conversations(updated_since=last_seen_at)["data"]
for conversation in page["data"]:
    print(conversation["conversation_id"], conversation["status"])

Email

The /v1/email surface is wrapped in full: sending domains, sender identities, lists and contacts, list imports, suppressions, templates, and campaigns. Paid and human-driven workflows (AI drafting, result exports) and deliverability triage (pausing a live send) run in the dashboard. Write methods need the email entitlement on the organization and return 403 ENTITLEMENT_REQUIRED without it. Reads are open.

Email lists are keyset paginated: the payload is { data, has_more, next_cursor }. Page until next_cursor is null, and never parse or construct a cursor.

There is no inbound email or inbox surface, and no A/B testing.

# Page through campaigns.
cursor = None
while True:
    page = client.list_email_campaigns(limit=100, cursor=cursor)
    for campaign in page["data"]["data"]:
        print(campaign["name"], campaign["status"])
    cursor = page["data"]["next_cursor"]
    if cursor is None:
        break

# Check what is blocking a campaign before scheduling it.
campaign = client.get_email_campaign(campaign_id)["data"]
if campaign.get("blocked"):
    for reason in campaign["blocked"]:
        print(reason["code"], reason["message"])
else:
    client.schedule_email_campaign(campaign_id, scheduled_at="2026-09-05T15:00:00Z")

Templates

Templates save the HTML a campaign sends. Create also returns lint: the save succeeds either way, but a campaign will not schedule while lint["errors"] is non-empty, so check it at save time rather than at send time. This endpoint only accepts HTML content; content["editor"] is "html" for a template created here, or "document" for one built in the dashboard's document editor, readable over the API as rendered HTML only. Updating the content of a "document" template returns 409 CONFLICT with details["reason"] set to "TEMPLATE_IS_DOCUMENT".

List imports

start_email_list_import fetches a CSV you host over https (50 MB cap) and commits it in one call. Omit mapping to let the server recognize a common ESP export; when neither your mapping nor the recognizer finds an email column the call is a 400 VALIDATION_ERROR whose details["headers"] lists the headers that were read, so you can retry with a mapping instead of guessing. The call returns 202; the import's progress is shown on the list in the dashboard.

started = client.start_email_list_import(
    "https://example.com/donors.csv",
    "lst_1",
    {"source": "donation_form", "note": "ActBlue donors, 2026 cycle"},
)["data"]
print(started["id"])

Error handling

Non-success responses raise PoliticalCommsError with the API's machine readable code, the HTTP status_code, and the raw response body.

from political_comms import PoliticalCommsClient, PoliticalCommsError

client = PoliticalCommsClient()
try:
    client.get_project("proj_unknown")
except PoliticalCommsError as err:
    print(err.code, err.status_code, err)

Network failures raise PoliticalCommsError with code == "NETWORK_ERROR" and status_code == 0.

Retries

The client retries automatically with these rules:

  • 400, 401, 403, 404 are never retried.
  • 429 is retried after waiting until the X-RateLimit-Reset timestamp.
  • 500, 502, 503, 504 are retried with exponential backoff and jitter: 1 second base, 60 second cap, at most 5 attempts total.

Configure the retry budget with max_retries (retries after the first attempt, default 4):

client = PoliticalCommsClient(max_retries=2)

Every POST and PATCH request carries an Idempotency-Key header (a random UUID) so retries are safe; the API returns the cached first response when a key is replayed. DELETE requests send the header only when you supply a key. Supply your own key per call when you need cross-process deduplication:

client.create_project(..., idempotency_key="send-2026-11-03-wave-1")

Rate limits

The API allows 600 requests per minute per key: bursts of up to 600 at once, refilling at 10 per second, the same for every scope. The client exposes the most recent rate limit headers:

client.list_organizations()
print(client.last_rate_limit)
# RateLimitState(limit=100, remaining=97, reset=1767225600)  (reset is Unix seconds)

License

MIT. Questions: support@politicalcomms.com

Metadata

Release files for political-comms 0.15.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for political-comms 0.15.0
File Size Uploaded Provenance Actions
political_comms-0.15.0.tar.gz 21.4 kB Without provenance Details for political_comms-0.15.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for political-comms 0.15.0
File Interpreter ABI Platform Actions
political_comms-0.15.0-py3-none-any.whl Python 3 none any Details for political_comms-0.15.0-py3-none-any.whl

Total release size: 39.4 kB

Release files / political_comms-0.15.0.tar.gz

Download URL political_comms-0.15.0.tar.gz
Size 21.4 kB
Tags Source
SHA-256 checksum
How to use checksums
4447ce49f9a826a4a60e51c8d45706141f73a7a5a61b6154a51a6b5b014aaec0
BLAKE2b-256 checksum
How to use checksums
61c82149fe8265cf7e98d31e45e2c711d18ba18e155e4b9379fbe3761beef8da
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.4

Release files / political_comms-0.15.0-py3-none-any.whl

Download URL political_comms-0.15.0-py3-none-any.whl
Size 18.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e9ec699c12f1af42fd705acd1b115300f3e6429ffacdb4aad67b8f75c1eb170e
BLAKE2b-256 checksum
How to use checksums
1127d92f9a5569082897bfa692fb251cfe1a14c60cbc7b9f7adedda5470a7bea
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.4

Security for political-comms 0.15.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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