Skip to main content

Python SDK for Vchasno.EDO (Electronic Document Management) API v2

Project description

py-vchasno

Disclaimer: This is an unofficial SDK, not affiliated with or endorsed by Vchasno. It was built based on publicly available information about the Vchasno.EDO API v2.

PyPI version Python 3.10+ License: MIT

Python SDK for Vchasno.EDO API v2 — Ukrainian electronic document management service.

Features

  • Sync & Async clients — Vchasno and AsyncVchasno (async-first with unasyncd)
  • Full API coverage — all 19 endpoint groups (documents, signatures, comments, reviews, tags, archive, cloud signer, etc.)
  • Transport hardening — automatic retry on network errors and 429 with full jitter exponential backoff
  • HTTPS enforcement — secure by default (transport-level allow_http=True for testing)
  • Streaming downloadsrequest_stream context manager for large files
  • Pydantic v2 models with full type annotations and extra="allow" for forward compatibility
  • py.typed — first-class support for mypy / pyright

Installation

pip

pip install py-vchasno

uv

uv add py-vchasno

poetry

poetry add py-vchasno

From source

git clone https://github.com/captainluzik/py-vchasno.git
cd py-vchasno
pip install .

Quick start

Sync client

from vchasno import Vchasno

with Vchasno(token="your-api-token") as client:
    # List signed documents
    docs = client.documents.list(status=7008)
    for doc in docs.documents:
        print(f"{doc.title}{doc.status_text}")

    # Upload a document
    result = client.documents.upload(
        "invoice.pdf",
        recipient_edrpou="12345678",
        category=2,
    )

    # Check counterparty registration
    info = client.company.check(edrpou="12345678")
    print(f"{info.name}: registered={info.is_registered}")

Async client

import asyncio
from vchasno import AsyncVchasno

async def main():
    async with AsyncVchasno(token="your-api-token") as client:
        docs = await client.documents.list(status=7008)
        incoming = await client.documents.list_incoming()
        print(f"Outgoing: {len(docs.documents)}, Incoming: {len(incoming.documents)}")

asyncio.run(main())

Authentication

Generate an API token in Vchasno.EDO settings. The token is sent as Authorization: <token> header.

client = Vchasno(token="your-api-token")

The base_url must use HTTPS (default: https://edo.vchasno.ua).

Optionally override the base URL and timeout:

client = Vchasno(
    token="your-api-token",
    base_url="https://edo.vchasno.ua",  # default; HTTPS required
    timeout=60.0,                        # seconds, default 30
    max_retries=5,                       # retry on 429 / network errors, default 3
)

Note: For local testing with HTTP, use the transport directly: SyncTransport(base_url="http://localhost", token="...", allow_http=True)

API reference

All endpoints are accessible as attributes of the client object. Each group provides both sync and async versions.

Documents — client.documents

# List outgoing documents with filters
docs = client.documents.list(
    status=7008,
    date_from="2024-01-01",
    date_to="2024-12-31",
    category=1,
    with_tags=True,
)

# Paginate with cursor
page = client.documents.list()
while page.next_cursor:
    page = client.documents.list(cursor=page.next_cursor)

# Get single document
doc = client.documents.get("document-uuid")

# Upload (multipart/form-data)
result = client.documents.upload(
    "contract.pdf",
    recipient_edrpou="12345678",
    recipient_emails="recipient@company.com",
    category=3,
    amount=1500000,  # amount in kopecks (= 15000.00 UAH)
    first_sign_by="owner",
)

# Upload from file object
with open("doc.pdf", "rb") as f:
    result = client.documents.upload(f, filename="doc.pdf")

# Edit document metadata (status < 7003)
client.documents.update_info(
    "document-uuid",
    title="Updated title",
    amount=500000,
    category=1,
)

# Edit recipient
client.documents.update_recipient("document-uuid", edrpou="87654321", email="new@mail.com")

# Access settings
client.documents.update_access_settings("document-uuid", level="private")
client.documents.update_viewers("document-uuid", strategy="add", roles_ids=["role-uuid"])

# Set multilateral signers
client.documents.set_flow("document-uuid", [
    {"edrpou": "11111111", "emails": ["signer@a.com"], "order": 0, "sign_num": 1},
    {"edrpou": "22222222", "emails": ["signer@b.com"], "order": 1, "sign_num": 1},
])

# List incoming documents
incoming = client.documents.list_incoming(
    status=7004,
    date_created_from="2024-01-01",
)

# Set signers for a document
client.documents.set_signers("document-uuid", signer_entities=[
    {"type": "role", "id": "role-uuid"},
    {"type": "group", "id": "group-uuid"},
], is_parallel=False)

# Download original file
content = client.documents.download_original("document-uuid")
with open("original.pdf", "wb") as f:
    f.write(content)

# Download specific version
content = client.documents.download_original("document-uuid", version="latest")

# Download ZIP archive with signatures
archive = client.documents.download_archive("document-uuid", with_instruction=1)

# Streaming download (large files, avoids loading into memory)
# async: async with client.documents.request_stream("GET", "/path") as stream: ...
# sync equivalent also available

# Download P7S / ASIC containers
p7s = client.documents.download_p7s("document-uuid")
asic = client.documents.download_asic("document-uuid")

# Batch download info
info = client.documents.download_documents(["uuid-1", "uuid-2"])

# XML to PDF
client.documents.xml_to_pdf_create("document-uuid", force=True)
pdf = client.documents.xml_to_pdf_download("document-uuid")

# PDF print view
printable = client.documents.pdf_print("document-uuid")

# Batch statuses (up to 500 IDs)
statuses = client.documents.statuses(["uuid-1", "uuid-2"])
for s in statuses.data_list:
    print(f"{s.document_id}: {s.status_text}")

# Reject a document
client.documents.reject("document-uuid", text="Incorrect amount")

# Send after signing
client.documents.send("document-uuid")

# Delete
client.documents.delete("document-uuid")

# Archive / unarchive
client.documents.archive(["uuid-1", "uuid-2"], directory_id="dir-uuid")
client.documents.unarchive(["uuid-1", "uuid-2"])

# Mark as processed
result = client.documents.mark_as_processed(["uuid-1", "uuid-2"])

# Structured data (sd_status must be confirmed/downloaded)
sd = client.documents.structured_data_download("document-uuid", output_format="json")

Signatures — client.signatures

# List signatures for a document
sigs = client.signatures.list("document-uuid")
for sig in sigs:
    print(f"{sig.signer_name} ({sig.edrpou}) at {sig.timestamp}")

# Add a detached signature (base64-encoded .p7s)
client.signatures.add("document-uuid", signature="base64...", stamp="base64...")

# Multilateral document flows
flows = client.signatures.flows("document-uuid")

Comments — client.comments

# All comments across documents
comments = client.comments.list(date_from="2024-01-01")

# Comments for a specific document
doc_comments = client.comments.list_for_document("document-uuid")

# Add a comment
client.comments.add("document-uuid", text="Please review", is_internal=True)

Reviews (Approval) — client.reviews

# Approval history
history = client.reviews.history("document-uuid")

# Current review requests
requests = client.reviews.requests("document-uuid")

# Overall status
status = client.reviews.status("document-uuid")
print(f"{status.status}, required={status.is_required}")

# Add / remove reviewer
client.reviews.add_reviewer("document-uuid", user_to_email="reviewer@company.com")
client.reviews.add_reviewer("document-uuid", group_to_name="Accounting", is_parallel=False)
client.reviews.remove_reviewer("document-uuid", user_to_email="reviewer@company.com")

Versions — client.versions

# Upload a new version
client.versions.upload("document-uuid", "contract_v2.pdf")

# Delete last version
client.versions.delete("document-uuid", "version-uuid")

Delete Requests — client.delete_requests

# Create a delete request
client.delete_requests.create("document-uuid", message="Duplicate document")

# Cancel / accept / reject
client.delete_requests.cancel("document-uuid")
client.delete_requests.accept("document-uuid")
client.delete_requests.reject("document-uuid", reject_message="Not a duplicate")

# List delete requests
requests = client.delete_requests.list(status="new")

# Lock / unlock direct deletion
client.delete_requests.lock_delete(["uuid-1", "uuid-2"])
client.delete_requests.unlock_delete(["uuid-1", "uuid-2"])

Tags — client.tags

# List company tags
tags = client.tags.list(limit=100, offset=0)

# Roles linked to a tag
roles = client.tags.roles("tag-uuid")

# Create tags and assign to documents
new_tags = client.tags.create_for_documents(
    documents_ids=["doc-uuid"],
    names=["Urgent", "Q1-2024"],
)

# Connect / disconnect existing tags
client.tags.connect_documents(documents_ids=["doc-uuid"], tags_ids=["tag-uuid"])
client.tags.disconnect_documents(documents_ids=["doc-uuid"], tags_ids=["tag-uuid"])

# Tags for roles (employees)
client.tags.create_for_roles(roles_ids=["role-uuid"], names=["Manager"])
client.tags.connect_roles(roles_ids=["role-uuid"], tags_ids=["tag-uuid"])
client.tags.disconnect_roles(roles_ids=["role-uuid"], tags_ids=["tag-uuid"])

Archive — client.archive

# List archive directories
dirs = client.archive.directories(parent_id=None, search="2024")

# Upload scans
result = client.archive.upload_scans(["scan1.pdf", "scan2.pdf"], parent_id=11)

# Import signed document (external format: original + .p7s files)
result = client.archive.import_signed_external(
    "document.pdf",
    ["signature1.p7s", "signature2.p7s"],
    title="Contract",
    amount=1000000,
)

# Import signed document (internal format: .p7s or ASiC-E container)
result = client.archive.import_signed_internal("signed_container.p7s")

Categories — client.categories

# List all document categories
cats = client.categories.list()

# Create / update / delete internal category
client.categories.create(title="Custom Type")
client.categories.update(37, title="Renamed Type")
client.categories.delete(37)

Fields — client.fields

# List custom fields
fields = client.fields.list()

# Create a new field
field = client.fields.create(name="PO Number", field_type="text", is_required=True)

# Document fields
doc_fields = client.fields.list_for_document("document-uuid")
client.fields.add_to_document("document-uuid", field_id="field-uuid", value="PO-12345")

Children — client.children

# Link / unlink child documents
client.children.add("parent-uuid", "child-uuid")
client.children.remove("parent-uuid", "child-uuid")

Groups — client.groups

# CRUD groups
groups = client.groups.list()
group = client.groups.create(name="Accounting Team")
client.groups.update("group-uuid", name="Finance Team")
client.groups.delete("group-uuid")

# Members
members = client.groups.members("group-uuid")
client.groups.add_members("group-uuid", role_ids=["role-1", "role-2"])
client.groups.remove_members("group-uuid", group_members=["member-uuid"])

Roles — client.roles

# List active employees
roles = client.roles.list()
for r in roles.roles:
    print(f"{r.email}{r.position}")

# Update permissions / notifications
client.roles.update("role-uuid", can_sign_and_reject_document=True, user_role=8001)

# Invite / create coworkers
client.roles.invite_coworkers(emails=["new@company.com"])
client.roles.create_coworker(email="new@company.com", first_name="John", last_name="Doe")

# Delete employee
client.roles.delete("role-uuid")

# Token management
client.roles.create_tokens(emails=["user@company.com"], expire_days="365")
client.roles.delete_tokens(emails=["user@company.com"])

Templates — client.templates

templates = client.templates.list()
template = client.templates.get("template-uuid")

Reports — client.reports

# Request a report (max 30-day range)
report = client.reports.request_document_actions(date_from="2024-01-01", date_to="2024-01-31")

# Check status
status = client.reports.status(report.report_id)
if status.status == "ready":
    xlsx = client.reports.download(report.report_id)
    with open(status.filename, "wb") as f:
        f.write(xlsx)

# User actions report
report = client.reports.request_user_actions(date_from="2024-01-01", date_to="2024-01-31")

Cloud Signer (Vchasno.KEP) — client.cloud_signer

# Create signing session
session = client.cloud_signer.create_session(duration=3600, client_id="key-uuid")
print(f"Session: {session.auth_session_id}")

# Poll until ready
check = client.cloud_signer.check_session(auth_session_id=session.auth_session_id)
if check.status == "ready":
    token = check.token

# Sign a document
client.cloud_signer.sign_document(
    client_id="key-uuid",
    password="key-password",
    document_id="document-uuid",
    auth_session_token=token,
)

# Refresh token flow
session = client.cloud_signer.create_session(
    duration=3600, client_id="key-uuid", use_refresh_token=True,
)
result = client.cloud_signer.check_refresh_session(auth_session_id=session.auth_session_id)
refreshed = client.cloud_signer.refresh_token(
    auth_session_id=session.auth_session_id,
    refresh_token=result.refresh_token,
)

# Create view/sign session for personal cabinet
sign_session = client.cloud_signer.create_sign_session(
    document_id="document-uuid",
    edrpou="12345678",
    email="signer@company.com",
    session_type="sign_session",
    on_finish_url="https://your-app.com/done",
)
print(f"Redirect to: {sign_session.url}")

Billing — client.billing

# Activate 30-day trial
client.billing.activate_trial()

Company — client.company

# Check single counterparty
info = client.company.check(edrpou="12345678")

# Bulk check from .xlsx / .csv
result = client.company.check_upload("counterparties.xlsx")
for c in result.companies:
    print(f"{c.edrpou} {c.name}: {c.is_registered}")

Enums

The SDK provides enums for all known constants:

from vchasno.models.enums import (
    DocumentStatus,
    DocumentCategory,
    FirstSignBy,
    ReviewState,
    StructuredDataStatus,
    DeleteRequestStatus,
    CloudSignerSessionStatus,
    AccessSettingsLevel,
)

# Document statuses
DocumentStatus.UPLOADED          # 7000
DocumentStatus.READY_TO_SIGN     # 7001
DocumentStatus.FULLY_SIGNED      # 7008
DocumentStatus.ANNULLED          # 7011

# Document categories
DocumentCategory.CONTRACT        # 3
DocumentCategory.INVOICE         # 2
DocumentCategory.OTHER           # 15

Error handling

from vchasno import (
    Vchasno,
    VchasnoError,
    VchasnoAPIError,
    AuthenticationError,
    RateLimitError,
    NotFoundError,
    BadRequestError,
)

with Vchasno(token="xxx") as client:
    try:
        doc = client.documents.get("non-existent-id")
    except NotFoundError:
        print("Document not found")
    except AuthenticationError:
        print("Invalid or expired token")
    except RateLimitError:
        # Automatic retry handles most 429s;
        # this only fires after max_retries exhausted
        print("Rate limit exceeded after retries")
    except BadRequestError as e:
        print(f"Bad request: {e.response_body}")
    except VchasnoAPIError as e:
        print(f"API error {e.status_code}: {e}")
    except VchasnoError as e:
        print(f"SDK error: {e}")

Rate limiting

Vchasno API allows 10 requests/second per company. The SDK automatically retries 429 responses and transient network errors (httpx.TransportError, httpx.TimeoutException) with full jitter exponential backoff. The Retry-After header is honoured (capped at 60 s). Configure retries via max_retries (default 3).

Important notes

  • Amounts are always in kopecks (1 UAH = 100 kopecks). Example: amount=1500000 means 15,000.00 UAH.
  • Datetime format: YYYY-MM-DD or YYYY-MM-DDTHH:MM.
  • File limits: single file up to 15 MB; ZIP archive up to 500 files / 100 MB.
  • Pagination: use cursor / next_cursor pattern for all list endpoints.

Development

Async-First Development

This SDK uses async-first development with unasyncd.

  • Write code in src/vchasno/_async/ only
  • Run unasyncd to generate src/vchasno/_sync/
  • Never edit _sync/ files manually
  • CI verifies sync freshness: unasyncd --force && git diff --exit-code src/vchasno/_sync/

Running checks

pip install -e ".[dev]"
ruff check src/ tests/
mypy src/
pytest

License

MIT — see LICENSE.

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

py_vchasno-0.2.0.tar.gz (37.1 kB view details)

Uploaded Source

Built Distribution

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

py_vchasno-0.2.0-py3-none-any.whl (55.8 kB view details)

Uploaded Python 3

File details

Details for the file py_vchasno-0.2.0.tar.gz.

File metadata

  • Download URL: py_vchasno-0.2.0.tar.gz
  • Upload date:
  • Size: 37.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for py_vchasno-0.2.0.tar.gz
Algorithm Hash digest
SHA256 69b4627b57fb3e93ee079642791b23df1fa519a97615ac112490a3fdc5b48140
MD5 c8888800fd52cfab04e89c35febec6fb
BLAKE2b-256 01f327e5eef1d7fd2abbb4f19440acd17b9107d7ab12c5e2c3117c0c952b34d0

See more details on using hashes here.

File details

Details for the file py_vchasno-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: py_vchasno-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 55.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for py_vchasno-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1f00175bf6b681941ce6bea9e3393bea661c02ec8e6a1c97d5a846616610a39a
MD5 3727009bbe2f0503a1a61492925ebd06
BLAKE2b-256 b330de48a681bbad47022c50024bb4f11b7c30928d23b77f510f7cd2f5b4c6fd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page