Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sendly-python-client

Python client for the Sendly SMS API (REST endpoint: https://api.sendly.link).

  • Zero runtime dependencies (only urllib from the standard library), full type annotations, py.typed.
  • Python 3.10+.
  • No automatic retries: a retried request could send the SMS twice, so the client never retries requests on its own. If you retry a send on the application side, make sure first that the previous attempt definitely failed before the request was accepted.

Installation

pip install sendly-client

For development (tests):

pip install -e ".[dev]"

Configuration

Setting Constructor argument Environment variable Default value
API token token SENDLY_TOKEN – (required)
Base URL base_url SENDLY_BASE_URL https://api.sendly.link
HTTP timeout (s) timeout 30.0

Arguments passed explicitly to the constructor take precedence over environment variables. For backward compatibility, the legacy variables ACTIO_TOKEN and ACTIO_BASE_URL are also supported: the client reads them only when the corresponding SENDLY_* variable is not set (when both are set, SENDLY_* wins). The token is generated in the Sendly customer panel (or provided by customer support).

Both send_sms and send_sms_multi also accept an optional timeout parameter (in seconds) per call, which overrides the client's default value for that one request:

client = SendlyClient("your-api-token", timeout=30.0)
client.send_sms(from_="48732129000", to="48732129001", body="Test Sendly", timeout=5.0)

Request identification headers

Every outgoing request to the API includes:

  • User-Agent: sendly-python-client/<version> (Python/<major>.<minor>), e.g. sendly-python-client/2.0.0 (Python/3.14) – the version comes from sendly.__version__.
  • X-Request-Id: <uuid> – a fresh UUID v4 for every request, a correlation identifier for contacting customer support and clarifying suspected double-sends (a complement to the no-retry policy).

Quick start – sending a single SMS

from sendly import SendlyClient

client = SendlyClient("your-api-token")  # or set SENDLY_TOKEN

result = client.send_sms(from_="48732129000", to="48732129001", body="Test Sendly")
print(result.message_id)  # unique message id, e.g. "a906cff7719bd889"

Equivalent in curl:

curl -X POST https://api.sendly.link/api/sms \
  -H "Authorization: Bearer $SENDLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"from": "48732129000", "to": "48732129001", "body": "Test Sendly"}'

from is a Python keyword, so the parameter is called from_ (the JSON field sent to the API is still from).

Sending to multiple numbers

results = client.send_sms_multi(
    from_="48732129000",
    to=["48732129001", "48732129002"],
    body="Test Sendly",
)
for item in results:
    print(item.number, item.message_id)

Equivalent in curl:

curl -X POST https://api.sendly.link/api/sms-multi \
  -H "Authorization: Bearer $SENDLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"from": "48732129000", "to": ["48732129001", "48732129002"], "body": "Test Sendly"}'

to must contain 1-100 unique numbers. Server-side validation is all-or-nothing: every number must be a valid Polish mobile number, and the numbers must be unique – otherwise the whole request is rejected. This API method must additionally be activated for your token by Sendly customer support.

Parsing webhooks

Sendly sends incoming messages, and (once activated) delivery notifications, as POST requests to the webhook address configured in the customer panel. Delivery is a single attempt, redirects are not followed, and no authentication is performed against your webhook; requests originate from the current IP address of the api.sendly.link host.

parse_webhook accepts decoded JSON as a dict, or the raw request body as str/bytes, and returns a typed model:

from sendly import DeliveryNotification, IncomingMessage, parse_webhook

payload = parse_webhook(request_body)  # dict | str | bytes

if isinstance(payload, IncomingMessage):
    # {"type": "MESSAGE", "from": ..., "to": ..., "body": ...}
    print(f"SMS from {payload.from_} to {payload.to}: {payload.body}")
elif isinstance(payload, DeliveryNotification):
    # {"type": "NOTIFICATION", "message_id": ..., "status": "DELIVERED" | "ERROR"}
    print(f"Message {payload.message_id} is {payload.status.value}")

Invalid payloads and unknown type/status values raise SendlyValidationError. Note that IncomingMessage.from_ may be an alphanumeric sender name (a sender ID override), not only a string of digits.

Error handling

All library errors inherit from SendlyError:

  • SendlyValidationError – invalid input detected client-side, before any HTTP request is sent: from/to must be digit strings 9-11 characters long, body must not be empty, and to in a multi-send must contain 1-100 unique numbers.
  • SendlyApiError – the API responded with an error status (403 an authorization problem, 422 a validation problem). Carries status_code, a parsed errors map (optional keys token, from, to, body, each a list of messages), and raw (the raw response body text – a fallback for when the response is not JSON).

Network errors (DNS, connection, timeout) propagate as standard urllib.error.URLError / OSError.

from sendly import SendlyApiError, SendlyClient, SendlyValidationError

client = SendlyClient("your-api-token")
try:
    client.send_sms(from_="48732129000", to="48732129001", body="Test Sendly")
except SendlyValidationError as exc:
    print(f"Bad input, nothing was sent: {exc}")
except SendlyApiError as exc:
    if exc.status_code == 403:
        print("Authorization problem:", exc.errors.get("token", [exc.raw]))
    elif exc.status_code == 422:
        for field, messages in exc.errors.items():
            print(f"{field}: {'; '.join(messages)}")
    else:
        print(exc)

Encoding

Outgoing messages are encoded as UCS2 and split every 60 characters. GSM7 encoding can optionally be enabled (customer panel / customer support), with a single-message limit of up to 160 characters.

Out of scope

3CX SMS API mode (/api/tcx) is mutually exclusive with the REST SMS API and is not implemented in this client.

Development

python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/python -m pytest -q

Tests run against a local HTTP stub server and never query the real API.

License

MIT – Copyright (c) 2026 ACTIO. See LICENSE.

About

Python client for the Sendly SMS API – send SMS, bulk send, webhook parsing. Zero dependencies, Python 3.10+.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages