Python SDK
The Python SDK provides both async and sync clients for integrating with Subnoto’s API with Oak tunnel encryption and HTTP signature authentication.
Installation
Section titled “Installation”pip install subnoto-api-clientQuick Start
Section titled “Quick Start”Set your credentials as environment variables - SubnotoConfig() picks them up automatically.
export SUBNOTO_ACCESS_KEY="your-access-key"export SUBNOTO_SECRET_KEY="your-secret-key-hex"import asynciofrom subnoto_api_client import SubnotoClient, SubnotoConfig
async def main(): async with SubnotoClient(SubnotoConfig()) as client: response = await client.post("/public/utils/whoami", json={}) print(f"User: {response.json()}")
response = await client.post("/public/workspace/list", json={}) print(f"Workspaces: {response.json()}")
asyncio.run(main())from subnoto_api_client import SubnotoSyncClient, SubnotoConfig
with SubnotoSyncClient(SubnotoConfig()) as client: response = client.post("/public/utils/whoami", json={}) print(f"User: {response.json()}")
response = client.post("/public/workspace/list", json={}) print(f"Workspaces: {response.json()}")Configuration
Section titled “Configuration”All options are optional. Credentials fall back to environment variables when not provided explicitly.
| Option | Type | Default | Description |
|---|---|---|---|
access_key | str | SUBNOTO_ACCESS_KEY env var | API access key from your team settings |
secret_key | str | SUBNOTO_SECRET_KEY env var | API secret key (hex-encoded) from your team settings |
Credentials can also be passed explicitly when env vars are not convenient:
config = SubnotoConfig( access_key="your-access-key", secret_key="your-secret-key-hex",)Workspace and workspaceUuid
Section titled “Workspace and workspaceUuid”In most Public API endpoints, workspaceUuid is optional. When omitted, the team’s default workspace is used. This keeps code simple when you use a single workspace. To target a specific workspace, pass workspaceUuid in the request body.
Per-endpoint details are in the OpenAPI specifications. For a complete flow (create-from-file with Smart Anchors, send, iframe token) and error handling with SubnotoError and get_error_code, see the Subnoto SDK demo (Python) and its customer implementation guide.
Common Use Cases
Section titled “Common Use Cases”All examples below use the async client. For the sync version, replace SubnotoClient with SubnotoSyncClient, async with with with, and remove async/await keywords.
List Workspaces
Section titled “List Workspaces”async def list_workspaces(client): response = await client.post("/public/workspace/list", json={}) data = response.json()
for workspace in data.get("workspaces", []): print(f"Workspace: {workspace['name']} ({workspace['uuid']})")
return data["workspaces"]List Envelopes in a Workspace
Section titled “List Envelopes in a Workspace”Omit workspaceUuid to list envelopes in the default workspace, or pass it to scope to a specific workspace.
async def list_envelopes(client, workspace_uuid=None): body = {"page": 1} if workspace_uuid: body["workspaceUuid"] = workspace_uuid response = await client.post("/public/envelope/list", json=body) data = response.json()
for envelope in data.get("envelopes", []): print(f"Envelope: {envelope['title']} - Status: {envelope['status']}")
return data["envelopes"]Create an Envelope from Template
Section titled “Create an Envelope from Template”workspaceUuid is optional; omit it to use the default workspace.
async def create_envelope_from_template(client, template_uuid, recipients, workspace_uuid=None): body = {"templateUuid": template_uuid, "recipients": recipients} if workspace_uuid: body["workspaceUuid"] = workspace_uuid response = await client.post("/public/envelope/create-from-template", json=body) data = response.json()
print(f"Created envelope: {data['envelopeUuid']}") return data
recipients = [ { "type": "manual", "label": "customer", "firstname": "John", "lastname": "Doe" }]Create an Envelope from File
Section titled “Create an Envelope from File”Use a multipart upload to create an envelope directly from a PDF or Word document. workspaceUuid is optional; omit it to use the default workspace. The public client supports multipart: use client.post(..., data=..., files=...).
from pathlib import Path
async def create_envelope_from_file(client, file_path, title, workspace_uuid=None): with open(file_path, "rb") as f: file_buffer = f.read()
data = {"envelopeTitle": title} if workspace_uuid: data["workspaceUuid"] = workspace_uuid response = await client.post( "/public/envelope/create-from-file", data=data, files={"file": (Path(file_path).name, file_buffer, "application/pdf")}, ) result = response.json()
print(f"Created envelope: {result['envelopeUuid']}") return resultSend an Envelope
Section titled “Send an Envelope”workspaceUuid is optional; omit it to use the default workspace.
async def send_envelope(client, envelope_uuid, custom_message=None, workspace_uuid=None): body = {"envelopeUuid": envelope_uuid} if workspace_uuid: body["workspaceUuid"] = workspace_uuid if custom_message: body["customInvitationMessage"] = custom_message
response = await client.post("/public/envelope/send", json=body)
if response.status_code == 200: print(f"Envelope {envelope_uuid} sent successfully!") else: print(f"Failed to send envelope: {response.text}")List Templates
Section titled “List Templates”async def list_templates(client): response = await client.post("/public/template/list", json={"page": 1}) data = response.json()
for template in data.get("templates", []): print(f"Template: {template['title']} ({template['uuid']})")
return data["templates"]List Contacts
Section titled “List Contacts”workspaceUuid is optional; omit it to list contacts in the default workspace.
async def list_contacts(client, workspace_uuid=None): body = {} if workspace_uuid: body["workspaceUuid"] = workspace_uuid response = await client.post("/public/contact/list", json=body) data = response.json()
for contact in data.get("contacts", []): print(f"Contact: {contact['firstname']} {contact['lastname']} ({contact['email']})")
return data["contacts"]Get an Envelope
Section titled “Get an Envelope”Retrieve envelope details including documents, blocks, and recipients.
async def get_envelope(client, envelope_uuid): response = await client.post("/public/envelope/get", json={ "envelopeUuid": envelope_uuid }) data = response.json()
print(f"Envelope: {data['title']} - Status: {data['status']}") for doc in data.get("documents", []): print(f" Document: {doc['uuid']} ({doc['pageCount']} pages)") return dataAdd Recipients
Section titled “Add Recipients”Add recipients to a draft envelope. Recipients can be manual entries, existing contacts, or workspace members.
async def add_recipients(client, envelope_uuid): response = await client.post("/public/envelope/add-recipients", json={ "envelopeUuid": envelope_uuid, "recipients": [ { "type": "manual", "firstname": "John", "lastname": "Doe" } ] }) data = response.json()
for r in data.get("recipients", []): print(f" Added: {r['firstname']} {r['lastname']} ({r['email']})") return dataAdd Blocks
Section titled “Add Blocks”Add signature, text, date, or image blocks to a document. Blocks must be added while the envelope is in draft status.
async def add_blocks(client, envelope_uuid, document_uuid): response = await client.post("/public/envelope/add-blocks", json={ "envelopeUuid": envelope_uuid, "documentUuid": document_uuid, "blocks": [ { "type": "signature", "page": "1", "x": 100, "y": 500, }, { "type": "text", "page": "1", "x": 100, "y": 460, "width": 200, "height": 27, "text": "", "templatedText": "fullname", } ] }) return response.json()Templated text blocks: When you set templatedText on a text block, the text field is automatically populated from the recipient’s data at creation time. Available values:
"fullname"— resolved to the recipient’s first and last name"email"— resolved to the recipient’s email address"phone"— resolved to the recipient’s phone number
recipientEmail must be set and match an existing recipient on the envelope for templated text to resolve.
Delete an Envelope
Section titled “Delete an Envelope”async def delete_envelope(client, envelope_uuid): response = await client.post("/public/envelope/delete", json={ "envelopeUuid": envelope_uuid }) if response.status_code == 200: print(f"Envelope {envelope_uuid} deleted")Full Workflow Example
Section titled “Full Workflow Example”import asynciofrom pathlib import Pathfrom subnoto_api_client import SubnotoClient, SubnotoConfig
async def create_and_send_envelope(): async with SubnotoClient(SubnotoConfig()) as client: # 1. Create envelope from file (default workspace) with open("contract.pdf", "rb") as f: file_buffer = f.read()
response = await client.post( "/public/envelope/create-from-file", data={"envelopeTitle": "Service Contract"}, files={"file": ("contract.pdf", file_buffer, "application/pdf")}, ) result = response.json() envelope_uuid = result["envelopeUuid"]
# 2. Get the document UUID response = await client.post("/public/envelope/get", json={ "envelopeUuid": envelope_uuid }) document_uuid = response.json()["documents"][0]["uuid"]
# 3. Add recipients await client.post("/public/envelope/add-recipients", json={ "envelopeUuid": envelope_uuid, "recipients": [{ "type": "manual", "firstname": "Jane", "lastname": "Smith" }] })
# 4. Add signature and auto-filled name blocks await client.post("/public/envelope/add-blocks", json={ "envelopeUuid": envelope_uuid, "documentUuid": document_uuid, "blocks": [ { "type": "signature", "page": "1", "x": 100, "y": 500, }, { "type": "text", "page": "1", "x": 100, "y": 460, "width": 200, "height": 27, "text": "", "templatedText": "fullname", } ] })
# 5. Send the envelope await client.post("/public/envelope/send", json={ "envelopeUuid": envelope_uuid, "customInvitationMessage": "Please review and sign this document." })
print(f"Envelope {envelope_uuid} created and sent!")
asyncio.run(create_and_send_envelope())Using Generated API Types
Section titled “Using Generated API Types”The SDK includes generated API types for type-safe interactions. Each generated endpoint module exposes both .asyncio() and .sync() methods.
from subnoto_api_client import SubnotoClient, SubnotoConfigfrom subnoto_api_client.generated.api.utils import post_public_utils_whoamifrom subnoto_api_client.generated.api.workspace import post_public_workspace_listfrom subnoto_api_client.generated.models.post_public_utils_whoami_body import PostPublicUtilsWhoamiBodyfrom subnoto_api_client.generated.models.post_public_workspace_list_body import PostPublicWorkspaceListBody
async def main(): async with SubnotoClient(SubnotoConfig()) as client: response = await post_public_utils_whoami.asyncio( client=client.generated, body=PostPublicUtilsWhoamiBody() ) print(f"User info: {response.to_dict()}")
response = await post_public_workspace_list.asyncio( client=client.generated, body=PostPublicWorkspaceListBody() ) workspaces = response.to_dict().get('workspaces', []) print(f"Found {len(workspaces)} workspace(s)")from subnoto_api_client import SubnotoSyncClient, SubnotoConfigfrom subnoto_api_client.generated.api.utils import post_public_utils_whoamifrom subnoto_api_client.generated.api.workspace import post_public_workspace_listfrom subnoto_api_client.generated.models.post_public_utils_whoami_body import PostPublicUtilsWhoamiBodyfrom subnoto_api_client.generated.models.post_public_workspace_list_body import PostPublicWorkspaceListBody
with SubnotoSyncClient(SubnotoConfig()) as client: response = post_public_utils_whoami.sync( client=client.generated, body=PostPublicUtilsWhoamiBody() ) print(f"User info: {response.to_dict()}")
response = post_public_workspace_list.sync( client=client.generated, body=PostPublicWorkspaceListBody() ) workspaces = response.to_dict().get('workspaces', []) print(f"Found {len(workspaces)} workspace(s)")API Reference
Section titled “API Reference”For complete API documentation, see the OpenAPI specifications.
Package
Section titled “Package”- PyPI: subnoto-api-client
- License: Apache-2.0