Skip to content

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.

Terminal window
pip install subnoto-api-client

Set your credentials as environment variables - SubnotoConfig() picks them up automatically.

Terminal window
export SUBNOTO_ACCESS_KEY="your-access-key"
export SUBNOTO_SECRET_KEY="your-secret-key-hex"
import asyncio
from 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())

All options are optional. Credentials fall back to environment variables when not provided explicitly.

OptionTypeDefaultDescription
access_keystrSUBNOTO_ACCESS_KEY env varAPI access key from your team settings
secret_keystrSUBNOTO_SECRET_KEY env varAPI 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",
)

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.

All examples below use the async client. For the sync version, replace SubnotoClient with SubnotoSyncClient, async with with with, and remove async/await keywords.

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"]

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"]

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",
"email": "[email protected]",
"firstname": "John",
"lastname": "Doe"
}
]

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 result

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}")
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"]

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"]

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 data

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",
"email": "[email protected]",
"firstname": "John",
"lastname": "Doe"
}
]
})
data = response.json()
for r in data.get("recipients", []):
print(f" Added: {r['firstname']} {r['lastname']} ({r['email']})")
return data

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,
"recipientEmail": "[email protected]"
},
{
"type": "text",
"page": "1",
"x": 100,
"y": 460,
"width": 200,
"height": 27,
"text": "",
"templatedText": "fullname",
"recipientEmail": "[email protected]"
}
]
})
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.

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")
import asyncio
from pathlib import Path
from 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",
"email": "[email protected]",
"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,
"recipientEmail": "[email protected]"
},
{
"type": "text",
"page": "1",
"x": 100,
"y": 460,
"width": 200,
"height": 27,
"text": "",
"templatedText": "fullname",
"recipientEmail": "[email protected]"
}
]
})
# 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())

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, SubnotoConfig
from subnoto_api_client.generated.api.utils import post_public_utils_whoami
from subnoto_api_client.generated.api.workspace import post_public_workspace_list
from subnoto_api_client.generated.models.post_public_utils_whoami_body import PostPublicUtilsWhoamiBody
from 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)")

For complete API documentation, see the OpenAPI specifications.