Unofficial async Python SDK for the Podpislon (podpislon.ru) document signing API
Project description
podpislon-sdk
🇷🇺 Asynchronous Python SDK for the Podpislon document-signing API. Built on
httpx+pydantic v2. Ships first-class typing, rate limiting, retries, paginated iterators, and a webhook handler that drops straight into FastAPI / aiogram / aiohttp.
⚠️ Disclaimer
Этот проект — НЕОФИЦИАЛЬНЫЙ SDK.
Он создан и поддерживается сообществом. Этот SDK не является официальным, не имеет отношения к компании, владеющей сервисом https://podpislon.ru, не одобрен и не спонсирован ею. Все упомянутые торговые марки и названия принадлежат их законным владельцам.
Используете на свой страх и риск. Авторы и контрибьюторы не несут ответственности за любые последствия использования этой библиотеки.
This project is an UNOFFICIAL SDK.
It is community-maintained and is NOT affiliated with, endorsed by, sponsored by, or in any way officially connected with the company operating https://podpislon.ru. All trademarks and product names mentioned belong to their respective owners.
Use at your own risk. The authors and contributors are not liable for any consequences arising from the use of this library.
Содержание
- Установка
- Быстрый старт
- Аутентификация
- Работа с документами
- Информация о компании
- Платёжные системы
- Вебхуки
- Интеграция с FastAPI
- Интеграция с aiogram (телеграм-боты)
- Конфигурация клиента
- Обработка ошибок
- Логирование
- Внутреннее устройство
- Разработка и тестирование
- Релизы и версионирование
- Публикация на PyPI
- Contributing
- Лицензия
Установка
pip install podpislon-sdk
Опциональные «extras»:
# для готовых FastAPI-помощников
pip install "podpislon-sdk[fastapi]"
# для разработки (тесты, линтеры, type-checking)
pip install "podpislon-sdk[dev]"
Поддерживаемые версии Python: 3.9 — 3.13.
Быстрый старт
import asyncio
from pathlib import Path
from podpislon import PodpislonClient
async def main() -> None:
async with PodpislonClient(api_key="ВАШ_API_КЛЮЧ") as client:
# Сколько документов осталось на тарифе?
info = await client.company.get_info()
print(f"Компания: {info.company.name}, остаток: {info.signings_left}")
# Отправить документ на подпись
result = await client.documents.add(
name="Иван",
last_name="Иванов",
phone="+79991112233",
files=[Path("contract.pdf")],
)
print(f"Создан документ id={result.first_id}")
# Получить подписанный PDF и сохранить его
await client.documents.save_file(result.first_id, "signed.pdf")
asyncio.run(main())
Аутентификация
Создайте API-ключ в личном кабинете на странице «Интеграции» и передайте его в клиент:
client = PodpislonClient(api_key="ваш_api_ключ")
SDK автоматически добавляет заголовок X-Api-Key ко всем исходящим запросам.
Совет: не храните ключ в коде. Используйте переменные окружения и
os.environ["PODPISLON_API_KEY"], а в продакшене — менеджер секретов вашей платформы.
Работа с документами
Все эндпоинты «Document» доступны через client.documents.
Список документов
page = await client.documents.list(page=1)
print(f"Страница {page.pagination.current_page} из {page.pagination.page_count}")
print(f"Всего документов: {page.pagination.total_count}")
for doc in page:
print(doc.id, doc.name, doc.status_text)
Постраничный обход
iter_all сам обходит все страницы, пока не дойдёт до конца:
async for doc in client.documents.iter_all():
process(doc)
Фильтрация
from podpislon import DocumentStatus, Filter
page = await client.documents.list(
filter=Filter(
status=DocumentStatus.SIGNED,
fio="Иван Иванов",
phone="89999999999",
dates={">=": "1667941200", "<=": "1668041200"},
),
expand="package", # включить package_id (нужен для resend)
)
DocumentStatus экспортирует все возможные статусы (CREATED, SENT, OPENED, SIGNED, CANCEL_REQUESTED, CANCELLED) и для каждого даёт человекочитаемое описание:
DocumentStatus.SIGNED.description # "Подписан"
Отправка документа на подпись
from pathlib import Path
from podpislon import Contact, Payment
result = await client.documents.add(
name="Иван",
last_name="Иванов",
phone="+79991112233",
files=[Path("contract.pdf"), Path("addendum.pdf")],
second_name="Иванович", # опционально
contacts=[ # дополнительные подписанты
Contact(
name="Пётр",
last_name="Петров",
phone="+79111111111",
),
],
payment=Payment(pid="2", sum="1200"),
redirect_url="https://example.com/done",
sign_by_time=1744035750, # дедлайн (Unix timestamp)
)
print(result.ids) # [101, 102]
Получить ссылку вместо SMS
Передайте no_sms=True, чтобы получить готовые ссылки на подписание:
result = await client.documents.add(
name="Иван",
last_name="Иванов",
phone="+79991112233",
files=[Path("contract.pdf")],
no_sms=True,
)
for link in result.links:
print("Подписать:", link)
JSON вместо multipart
По умолчанию SDK отправляет файлы через multipart/form-data (без накладных расходов на base64). Если нужен JSON-режим — передайте use_multipart=False:
await client.documents.add(
name="Иван",
last_name="Иванов",
phone="+79991112233",
files=[b"%PDF-1.4 fake bytes"],
file_names=["contract.pdf"],
use_multipart=False,
)
Получение PDF
pdf_bytes = await client.documents.get_file(file_id=101)
Path("signed.pdf").write_bytes(pdf_bytes)
# Или короче:
await client.documents.save_file(101, "signed.pdf")
Переотправка ссылки
await client.documents.resend(
package_id="cb4b683", # берётся из document.package при expand="package"
contact="ODYxMjI=", # sid конкретного подписанта (опционально)
)
Удаление документа
await client.documents.delete(file_id=101)
Информация о компании
info = await client.company.get_info()
print(info.company.name) # ООО "Рога и копыта"
print(info.company.inn) # 1001982736
print(info.company.kpp) # 123456789
print(info.signings_left) # 259
Платёжные системы
systems = await client.payments.list_systems()
for system in systems:
print(system.id, system.name)
Вебхуки
Подпислон отправляет события на ваш URL в формате application/x-www-form-urlencoded. SDK даёт типобезопасный парсер и асинхронный диспетчер.
Парсинг событий
from podpislon import parse_event
event = parse_event(b"EVENT=DOCUMENT_SIGNED&FILE_ID=1234&COMPANY_ID=12&SIGNATURE=abc")
print(event.event) # WebhookEventType.DOCUMENT_SIGNED
print(event.file_id) # 1234
Поддерживаются все три типа событий из спецификации:
| Событие | Модель |
|---|---|
DOCUMENT_OPENED |
DocumentOpenedEvent |
DOCUMENT_SIGNED |
DocumentSignedEvent |
CLIENT_DATA_REQUEST_SUBMITTED |
ClientDataRequestSubmittedEvent |
Регистрация обработчиков
from podpislon import (
DocumentOpenedEvent,
DocumentSignedEvent,
WebhookEventType,
WebhookHandler,
)
handler = WebhookHandler()
@handler.on(WebhookEventType.DOCUMENT_SIGNED)
async def signed(event: DocumentSignedEvent) -> None:
await db.mark_signed(event.file_id)
@handler.on(WebhookEventType.DOCUMENT_OPENED)
async def opened(event: DocumentOpenedEvent) -> None:
print(f"Документ {event.file_id} открыт телефоном {event.contact}")
@handler.on_any
async def audit(event) -> None:
await audit_log.write(event)
Обработчики могут быть как async, так и обычными функциями.
Проверка подписи
Алгоритм подписи Подпислона официально не задокументирован. Если служба поддержки сообщит вам нужный алгоритм — передайте его в
compute=. По умолчанию используется HMAC-SHA256 с секретом из аргумента.
from podpislon import WebhookHandler, WebhookSignatureVerifier
handler = WebhookHandler(
signature_verifier=WebhookSignatureVerifier(secret="ВАШ_СЕКРЕТ"),
)
# или со своим алгоритмом:
verifier = WebhookSignatureVerifier(
secret="ВАШ_СЕКРЕТ",
compute=lambda raw, key: my_custom_signature(raw, key),
)
Интеграция с FastAPI
from fastapi import FastAPI, Request, Response, HTTPException
from podpislon import (
DocumentSignedEvent,
PodpislonWebhookError,
WebhookEventType,
WebhookHandler,
)
app = FastAPI()
handler = WebhookHandler()
@handler.on(WebhookEventType.DOCUMENT_SIGNED)
async def on_signed(event: DocumentSignedEvent) -> None:
# ваша бизнес-логика
print("Подписан документ", event.file_id)
@app.post("/webhooks/podpislon")
async def podpislon_webhook(request: Request) -> Response:
body = await request.body()
try:
await handler.dispatch(body, raw_body=body)
except PodpislonWebhookError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return Response(status_code=200)
И клиент рядом, чтобы инициировать новую отправку:
from contextlib import asynccontextmanager
from podpislon import PodpislonClient
@asynccontextmanager
async def lifespan(app):
app.state.podpislon = PodpislonClient(api_key=os.environ["PODPISLON_API_KEY"])
try:
yield
finally:
await app.state.podpislon.aclose()
app = FastAPI(lifespan=lifespan)
@app.post("/contracts/{contract_id}/sign")
async def send_to_sign(contract_id: int, request: Request):
client = request.app.state.podpislon
result = await client.documents.add(
name="Иван",
last_name="Иванов",
phone="+79991112233",
files=[await render_contract_pdf(contract_id)],
no_sms=True,
)
return {"ids": result.ids, "links": result.links}
Интеграция с aiogram (телеграм-боты)
from aiogram import Bot, Dispatcher, F, Router
from aiogram.filters import CommandStart
from aiogram.types import Message
from podpislon import PodpislonClient
router = Router()
podpislon = PodpislonClient(api_key="...")
@router.message(CommandStart())
async def start(message: Message) -> None:
await message.answer("Пришлите PDF-документ — отправлю его на подпись.")
@router.message(F.document)
async def send_to_sign(message: Message, bot: Bot) -> None:
file = await bot.get_file(message.document.file_id)
pdf_bytes = await bot.download_file(file.file_path)
result = await podpislon.documents.add(
name="Иван",
last_name="Иванов",
phone="+79991112233",
files=[pdf_bytes.read()],
file_names=[message.document.file_name or "document.pdf"],
no_sms=True,
)
for link in result.links:
await message.answer(f"Ссылка для подписания: {link}")
async def main() -> None:
bot = Bot(token="TELEGRAM_BOT_TOKEN")
dp = Dispatcher()
dp.include_router(router)
try:
await dp.start_polling(bot)
finally:
await podpislon.aclose()
await bot.session.close()
Конфигурация клиента
from httpx import Timeout
from podpislon import PodpislonClient
client = PodpislonClient(
api_key="...",
base_url="https://podpislon.ru/integration", # переопределить редко нужно
timeout=Timeout(30.0, connect=5.0),
max_retries=3, # ретраи 429/5xx с экспоненциальной паузой
rate_limit=4, # клиентская защита (4 RPS)
user_agent="my-app/1.2.3",
)
Если у вас уже есть свой httpx.AsyncClient (например, c прокси или mTLS), передайте его в http_client=:
import httpx
from podpislon import PodpislonClient
shared = httpx.AsyncClient(proxies="http://proxy:3128")
client = PodpislonClient(api_key="...", http_client=shared)
# ВАЖНО: при пользовательском http_client SDK НЕ закроет его сам.
Обработка ошибок
Все исключения наследуются от PodpislonError:
from podpislon import (
PodpislonAuthenticationError, # HTTP 401
PodpislonPermissionError, # HTTP 403
PodpislonNotFoundError, # HTTP 404
PodpislonRateLimitError, # HTTP 429 после исчерпания ретраев
PodpislonServerError, # HTTP 5xx
PodpislonAPIError, # любой иной API-ответ с ошибкой
PodpislonValidationError, # ошибка валидации параметров на стороне клиента
PodpislonTransportError, # таймаут / сеть / DNS
PodpislonError, # базовый класс
)
try:
await client.documents.delete(123)
except PodpislonNotFoundError:
print("Документ не найден")
except PodpislonAuthenticationError:
print("Неверный API-ключ")
except PodpislonError as exc:
print(f"Что-то пошло не так: {exc}")
У PodpislonAPIError есть полезные атрибуты: status_code, response_body, request_id.
Логирование
SDK пишет отладочные сообщения в стандартный модуль logging под именем podpislon:
import logging
logging.getLogger("podpislon").setLevel(logging.DEBUG)
Внутреннее устройство
podpislon/
├── client.py # PodpislonClient — фасад
├── _transport.py # httpx-обёртка: rate limit, ретраи, маппинг ошибок
├── _utils.py # rate-limiter, base64, form-encoding helpers
├── exceptions.py # иерархия PodpislonError
├── enums.py # DocumentStatus, WebhookEventType
├── models/ # pydantic v2 модели
├── resources/ # Documents / Company / Payments
└── webhooks/ # parser + handler + signature verifier
Ключевые свойства:
- Async-first. Под капотом
httpx.AsyncClient. Никаких блокирующих вызовов на горячем пути. - Type-safe. Pydantic v2 для всех моделей, экспортируется
py.typedмаркер. - Rate-limiter из коробки. Скользящее окно 4 RPS на ключ — соответствует ограничению API.
- Ретраи с экспоненциальным backoff на
429и5xx, с уважением к заголовкуRetry-After. - Никаких глобальных стейтов. Один клиент = один пул соединений; легко работать с несколькими ключами одновременно.
- Webhook-handler. Декораторный API в духе
aiogram.
Разработка и тестирование
git clone https://github.com/IceOne-i/podpislon-sdk.git
cd podpislon-sdk
python -m venv .venv
source .venv/bin/activate # Linux / macOS
# .venv\Scripts\activate # Windows
pip install -e ".[dev]"
# тесты с покрытием
pytest --cov=podpislon
# линт и формат
ruff check src tests
ruff format src tests
# type-checking
mypy src
Релизы и версионирование
Версии собираются автоматически из git-тегов с помощью hatch-vcs. Чтобы выпустить новую версию:
git tag v1.2.3
git push origin v1.2.3
GitHub Actions подхватит тег, соберёт sdist + wheel и опубликует пакет на PyPI (см. workflow ниже).
Соблюдается SemVer:
MAJOR— несовместимые изменения публичного API;MINOR— обратносовместимые новые возможности;PATCH— обратносовместимые исправления.
Публикация на PyPI
В репозитории два workflow:
.github/workflows/tests.yml— на каждый PR/push прогоняет тесты на Python 3.9 — 3.13..github/workflows/publish.yml— на каждый push тегаv*собирает дистрибутивы и публикует их на PyPI через Trusted Publishing (без хранения токенов в репозитории).
Чтобы включить публикацию:
- Зарегистрируйте проект на PyPI (
pypi.org/manage/account/publishing/) — добавьте Trusted Publisher с owner =IceOne-i, repo =podpislon-sdk, workflow =publish.yml, environment =pypi. - В GitHub-репозитории создайте окружение
pypi(Settings → Environments). - Запушьте тег
vX.Y.Z— workflow сработает сам.
Если вы предпочитаете API-токен вместо Trusted Publishing, добавьте в секреты репозитория PYPI_API_TOKEN и раскомментируйте соответствующий шаг в publish.yml.
Contributing
Pull requests welcome! Но прежде чем большой PR — заведите issue с обсуждением.
- Форкните репозиторий и создайте ветку.
- Установите dev-зависимости:
pip install -e ".[dev]". - Добавьте тесты на новый функционал.
- Прогоните
pytest,ruff,mypy. - Откройте PR с осмысленным описанием.
Лицензия
MIT — самая разрешительная из распространённых лицензий. Можно использовать в коммерческих и закрытых проектах при условии сохранения исходного уведомления об авторских правах. Подробнее в файле LICENSE.
Полезные ссылки
- Сайт сервиса (не аффилирован с этим SDK): https://podpislon.ru/
- OpenAPI спецификация: https://api.podpislon.ru/podpislon-api.yaml
- Личный кабинет → Интеграции: https://podpislon.ru/lk/integrations
- Issues / запросы: https://github.com/IceOne-i/podpislon-sdk/issues
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 podpislon_sdk-1.3.0.tar.gz.
File metadata
- Download URL: podpislon_sdk-1.3.0.tar.gz
- Upload date:
- Size: 40.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
be08902bae7d23ed6f574e9a4ba18eec1b1e454a5aefb14ead60ba024364e70e
|
|
| MD5 |
453b610f2489ec1e91c98a5199d41e20
|
|
| BLAKE2b-256 |
38bdca7906207ab7d4eb99a4457c66f634bcfc557ecb677c4f94a9a35658c803
|
Provenance
The following attestation bundles were made for podpislon_sdk-1.3.0.tar.gz:
Publisher:
publish.yml on IceOne-i/podpislon-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
podpislon_sdk-1.3.0.tar.gz -
Subject digest:
be08902bae7d23ed6f574e9a4ba18eec1b1e454a5aefb14ead60ba024364e70e - Sigstore transparency entry: 1396826020
- Sigstore integration time:
-
Permalink:
IceOne-i/podpislon-sdk@d6ededab202bc9afc678ae9fcbed52603ae8632e -
Branch / Tag:
refs/tags/v1.3.0 - Owner: https://github.com/IceOne-i
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d6ededab202bc9afc678ae9fcbed52603ae8632e -
Trigger Event:
push
-
Statement type:
File details
Details for the file podpislon_sdk-1.3.0-py3-none-any.whl.
File metadata
- Download URL: podpislon_sdk-1.3.0-py3-none-any.whl
- Upload date:
- Size: 42.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
321e689e7afbad9c25d213a10e006155b94027ece55391d0d0ceadb294a645e8
|
|
| MD5 |
625515a1676aac824095dd8500a5fc0b
|
|
| BLAKE2b-256 |
b1df25582f76175d55f535b5b7671530690059ccb27c04e1d0148d2730fdc771
|
Provenance
The following attestation bundles were made for podpislon_sdk-1.3.0-py3-none-any.whl:
Publisher:
publish.yml on IceOne-i/podpislon-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
podpislon_sdk-1.3.0-py3-none-any.whl -
Subject digest:
321e689e7afbad9c25d213a10e006155b94027ece55391d0d0ceadb294a645e8 - Sigstore transparency entry: 1396826133
- Sigstore integration time:
-
Permalink:
IceOne-i/podpislon-sdk@d6ededab202bc9afc678ae9fcbed52603ae8632e -
Branch / Tag:
refs/tags/v1.3.0 - Owner: https://github.com/IceOne-i
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d6ededab202bc9afc678ae9fcbed52603ae8632e -
Trigger Event:
push
-
Statement type: