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
pip install subnoto-api-clientQuick 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
For complete API documentation, see the OpenAPI specifications.
Package
- PyPI: subnoto-api-client
- License: Apache-2.0