Skip to main content

Official Python SDK for Rynko - Generate PDFs and Excel files from templates

Project description

rynko

Official Python SDK for Rynko - the document generation platform with unified template design for PDF and Excel documents.

PyPI version Python versions License: MIT

Table of Contents

Installation

pip install rynko

Or with optional async support:

pip install rynko[async]

Quick Start

from rynko import Rynko

client = Rynko(api_key="your_api_key")

# Generate a PDF document (async - returns job info immediately)
job = client.documents.generate_pdf(
    template_id="tmpl_invoice",
    variables={
        "customerName": "John Doe",
        "invoiceNumber": "INV-001",
        "total": 150.00,
    },
)

print(f"Job ID: {job['jobId']}")
print(f"Status: {job['status']}")  # 'queued'

# Wait for completion to get the download URL
completed = client.documents.wait_for_completion(job["jobId"])
print(f"Download URL: {completed['downloadUrl']}")

Features

  • Sync and async clients - Choose based on your application needs
  • Full type hints - Complete type annotation support for IDE autocompletion
  • PDF generation - Generate PDF documents from templates
  • Excel generation - Generate Excel spreadsheets from templates
  • Batch generation - Generate multiple documents in a single request
  • Workspace support - Generate documents in specific workspaces
  • Webhook verification - Secure HMAC signature verification for incoming webhooks
  • Polling utility - Built-in wait_for_completion() method with configurable timeout
  • Context manager support - Automatic resource cleanup

Authentication

Get an API Key

  1. Log in to your Rynko Dashboard
  2. Navigate to SettingsAPI Keys
  3. Click Create API Key
  4. Copy the key and store it securely (it won't be shown again)

Initialize the Client

import os
from rynko import Rynko

# Using environment variable (recommended)
client = Rynko(api_key=os.environ["RYNKO_API_KEY"])

# Verify authentication
user = client.me()
print(f"Authenticated as: {user['email']}")
print(f"Team: {user.get('teamName')}")

Verify API Key

# Check if API key is valid
is_valid = client.verify_api_key()
print(f"API Key valid: {is_valid}")

Document Generation

Document generation in Rynko is asynchronous. When you call a generate method, the job is queued for processing and you receive a job ID immediately. Use wait_for_completion() to poll until the document is ready.

Generate PDF

# Queue PDF generation
job = client.documents.generate_pdf(
    template_id="tmpl_invoice",
    variables={
        "invoiceNumber": "INV-001",
        "customerName": "John Doe",
        "customerEmail": "john@example.com",
        "items": [
            {"description": "Product A", "quantity": 2, "price": 50.00},
            {"description": "Product B", "quantity": 1, "price": 50.00},
        ],
        "subtotal": 150.00,
        "tax": 15.00,
        "total": 165.00,
    },
)

print(f"Job queued: {job['jobId']}")
print(f"Status: {job['status']}")  # 'queued'

# Wait for completion
completed = client.documents.wait_for_completion(job["jobId"])
print(f"Download URL: {completed['downloadUrl']}")

Generate Excel

job = client.documents.generate_excel(
    template_id="tmpl_sales_report",
    variables={
        "reportTitle": "Q1 2026 Sales Report",
        "reportDate": "2026-03-31",
        "salesData": [
            {"region": "North", "q1": 125000, "q2": 0, "q3": 0, "q4": 0},
            {"region": "South", "q1": 98000, "q2": 0, "q3": 0, "q4": 0},
            {"region": "East", "q1": 145000, "q2": 0, "q3": 0, "q4": 0},
            {"region": "West", "q1": 112000, "q2": 0, "q3": 0, "q4": 0},
        ],
        "totalSales": 480000,
    },
)

completed = client.documents.wait_for_completion(job["jobId"])
print(f"Excel file ready: {completed['downloadUrl']}")

Generate with Options

The generate() method supports all document formats and advanced options:

job = client.documents.generate(
    # Required
    template_id="tmpl_contract",
    format="pdf",  # 'pdf' | 'excel' | 'csv'

    # Template variables
    variables={
        "contractNumber": "CTR-2026-001",
        "clientName": "Acme Corporation",
        "startDate": "2026-02-01",
        "endDate": "2027-01-31",
    },

    # Optional settings
    filename="contract-acme-2026",  # Custom filename (without extension)
    workspace_id="ws_abc123",       # Generate in specific workspace
    webhook_url="https://your-app.com/webhooks/document-ready",  # Webhook notification
    metadata={                       # Custom metadata (passed to webhook)
        "orderId": "ORD-12345",
        "userId": "user_abc",
    },
    use_draft=False,                 # Use draft template version (for testing)
    use_credit=False,                # Force use of purchased credits
)

Batch Generation

Generate multiple documents from a single template:

# Each dict in the documents list contains variables for one document
batch = client.documents.generate_batch(
    template_id="tmpl_invoice",
    format="pdf",
    documents=[
        {
            "invoiceNumber": "INV-001",
            "customerName": "John Doe",
            "total": 150.00,
        },
        {
            "invoiceNumber": "INV-002",
            "customerName": "Jane Smith",
            "total": 275.50,
        },
        {
            "invoiceNumber": "INV-003",
            "customerName": "Bob Wilson",
            "total": 89.99,
        },
    ],
    webhook_url="https://your-app.com/webhooks/batch-complete",
)

print(f"Batch ID: {batch['batchId']}")
print(f"Total jobs: {batch['totalJobs']}")  # 3
print(f"Status: {batch['status']}")  # 'queued'
print(f"Estimated wait: {batch['estimatedWaitSeconds']} seconds")

Wait for Completion

The wait_for_completion() method polls the job status until it completes or fails:

# Default settings (1 second interval, 30 second timeout)
completed = client.documents.wait_for_completion(job["jobId"])

# Custom polling settings
completed = client.documents.wait_for_completion(
    job["jobId"],
    poll_interval=2.0,   # Check every 2 seconds
    timeout=60.0,        # Wait up to 60 seconds
)

# Check result
if completed["status"] == "completed":
    print(f"Download URL: {completed['downloadUrl']}")
    print(f"File size: {completed['fileSize']} bytes")
    print(f"Expires at: {completed['downloadUrlExpiresAt']}")
elif completed["status"] == "failed":
    print(f"Generation failed: {completed['errorMessage']}")
    print(f"Error code: {completed['errorCode']}")

Document Jobs

Get Job Status

job = client.documents.get_job("job_abc123")

print(f"Status: {job['status']}")
# Possible values: 'queued' | 'processing' | 'completed' | 'failed'

print(f"Template: {job.get('templateName')}")
print(f"Format: {job['format']}")
print(f"Created: {job['createdAt']}")

if job["status"] == "completed":
    print(f"Download URL: {job['downloadUrl']}")
    print(f"File size: {job['fileSize']}")
    print(f"URL expires: {job['downloadUrlExpiresAt']}")

if job["status"] == "failed":
    print(f"Error: {job['errorMessage']}")
    print(f"Error code: {job['errorCode']}")

List Jobs

# List recent jobs with pagination
result = client.documents.list_jobs(limit=20, page=1)
jobs = result["data"]
meta = result["meta"]

print(f"Total jobs: {meta['total']}")
print(f"Pages: {meta['totalPages']}")

for job in jobs:
    print(f"{job['jobId']}: {job['status']} - {job.get('templateName')}")

# Filter by status
result = client.documents.list_jobs(status="completed")

# Filter by format
result = client.documents.list_jobs(format="pdf")

# Filter by template
result = client.documents.list_jobs(template_id="tmpl_invoice")

# Filter by workspace
result = client.documents.list_jobs(workspace_id="ws_abc123")

# Filter by date range
result = client.documents.list_jobs(
    date_from="2026-01-01",
    date_to="2026-01-31",
)

# Combine filters
result = client.documents.list_jobs(
    status="completed",
    format="pdf",
    template_id="tmpl_invoice",
    limit=50,
)

Templates

List Templates

# List all templates
result = client.templates.list()
templates = result["data"]
meta = result["meta"]

print(f"Total templates: {meta['total']}")

for template in templates:
    print(f"{template['id']}: {template['name']} ({template['type']})")

# Paginated list
result = client.templates.list(page=2, limit=10)

# Search by name
result = client.templates.list(search="invoice")

# List PDF templates only
result = client.templates.list_pdf()

# List Excel templates only
result = client.templates.list_excel()

Get Template Details

# Get template by ID (supports UUID, shortId, or slug)
template = client.templates.get("tmpl_invoice")

print(f"Template: {template['name']}")
print(f"Type: {template['type']}")  # 'pdf' | 'excel'
print(f"Description: {template.get('description')}")
print(f"Created: {template['createdAt']}")
print(f"Updated: {template['updatedAt']}")

# View template variables
if "variables" in template and template["variables"]:
    print("\nVariables:")
    for variable in template["variables"]:
        print(f"  {variable['name']} ({variable['type']})")
        print(f"    Required: {variable.get('required', False)}")
        if "defaultValue" in variable:
            print(f"    Default: {variable['defaultValue']}")

Webhooks

Webhook subscriptions are managed through the Rynko Dashboard. The SDK provides read-only access to view webhooks and utilities for signature verification.

List Webhooks

result = client.webhooks.list()
webhooks = result.get("data", [])

for webhook in webhooks:
    print(f"{webhook['id']}: {webhook['url']}")
    print(f"  Events: {', '.join(webhook['events'])}")
    print(f"  Active: {webhook['isActive']}")
    print(f"  Created: {webhook['createdAt']}")

Get Webhook Details

webhook = client.webhooks.get("wh_abc123")

print(f"URL: {webhook['url']}")
print(f"Events: {webhook['events']}")
print(f"Active: {webhook['isActive']}")
print(f"Description: {webhook.get('description')}")

Verify Webhook Signatures

When receiving webhooks, always verify the signature to ensure the request came from Rynko:

import os
from rynko import verify_webhook_signature, WebhookSignatureError

# Flask example
@app.route('/webhooks/rynko', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Rynko-Signature')
    timestamp = request.headers.get('X-Rynko-Timestamp')

    try:
        event = verify_webhook_signature(
            payload=request.data.decode('utf-8'),
            signature=signature,
            timestamp=timestamp,  # Optional but recommended for replay protection
            secret=os.environ['WEBHOOK_SECRET'],
        )

        # Process the verified event
        print(f"Event type: {event['type']}")
        print(f"Event ID: {event['id']}")
        print(f"Timestamp: {event['timestamp']}")

        if event['type'] == 'document.generated':
            job_id = event['data']['jobId']
            download_url = event['data']['downloadUrl']
            print(f"Document {job_id} ready: {download_url}")
            # Download or process the document

        elif event['type'] == 'document.failed':
            job_id = event['data']['jobId']
            error = event['data']['error']
            print(f"Document {job_id} failed: {error}")
            # Handle failure (retry, notify user, etc.)

        elif event['type'] == 'document.downloaded':
            job_id = event['data']['jobId']
            print(f"Document {job_id} was downloaded")

        return 'OK', 200

    except WebhookSignatureError as e:
        print(f"Invalid webhook signature: {e}")
        return 'Invalid signature', 401

Django Example

import os
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from rynko import verify_webhook_signature, WebhookSignatureError

@csrf_exempt
def webhook_handler(request):
    signature = request.headers.get('X-Rynko-Signature')
    timestamp = request.headers.get('X-Rynko-Timestamp')

    try:
        event = verify_webhook_signature(
            payload=request.body.decode('utf-8'),
            signature=signature,
            timestamp=timestamp,
            secret=os.environ['WEBHOOK_SECRET'],
        )

        # Process the event
        if event['type'] == 'document.generated':
            # Handle document completion
            pass

        return HttpResponse('OK', status=200)

    except WebhookSignatureError:
        return HttpResponse('Invalid signature', status=401)

FastAPI Example

import os
from fastapi import FastAPI, Request, HTTPException
from rynko import verify_webhook_signature, WebhookSignatureError

app = FastAPI()

@app.post("/webhooks/rynko")
async def webhook_handler(request: Request):
    signature = request.headers.get('X-Rynko-Signature')
    timestamp = request.headers.get('X-Rynko-Timestamp')
    body = await request.body()

    try:
        event = verify_webhook_signature(
            payload=body.decode('utf-8'),
            signature=signature,
            timestamp=timestamp,
            secret=os.environ['WEBHOOK_SECRET'],
        )

        # Process the event
        if event['type'] == 'document.generated':
            # Handle document completion
            pass

        return {"status": "ok"}

    except WebhookSignatureError:
        raise HTTPException(status_code=401, detail="Invalid signature")

Webhook Event Types

Event Description Payload
document.generated Document successfully generated jobId, templateId, format, downloadUrl, fileSize
document.failed Document generation failed jobId, templateId, error, errorCode
document.downloaded Document was downloaded jobId, downloadedAt

Webhook Headers

Rynko sends these headers with each webhook request:

Header Description
X-Rynko-Signature HMAC-SHA256 signature (format: v1=<hex>)
X-Rynko-Timestamp Unix timestamp when the webhook was sent
X-Rynko-Event-Id Unique event identifier
X-Rynko-Event-Type Event type (e.g., document.generated)

Async Client

For async applications (FastAPI, aiohttp, etc.), use AsyncRynko:

from rynko import AsyncRynko
import asyncio

async def main():
    async with AsyncRynko(api_key="your_api_key") as client:
        # Get user info
        user = await client.me()
        print(f"Authenticated as: {user['email']}")

        # Queue document generation
        job = await client.documents.generate_pdf(
            template_id="tmpl_invoice",
            variables={"invoiceNumber": "INV-001"},
        )
        print(f"Job queued: {job['jobId']}")

        # Wait for completion
        completed = await client.documents.wait_for_completion(job["jobId"])
        print(f"Download URL: {completed['downloadUrl']}")

        # List templates
        result = await client.templates.list()
        for template in result["data"]:
            print(f"Template: {template['name']}")

asyncio.run(main())

Async with FastAPI

from fastapi import FastAPI, Depends
from rynko import AsyncRynko
import os

app = FastAPI()

async def get_rynko():
    async with AsyncRynko(api_key=os.environ["RYNKO_API_KEY"]) as client:
        yield client

@app.post("/generate-invoice")
async def generate_invoice(
    invoice_data: dict,
    client: AsyncRynko = Depends(get_rynko)
):
    job = await client.documents.generate_pdf(
        template_id="tmpl_invoice",
        variables=invoice_data,
    )

    completed = await client.documents.wait_for_completion(job["jobId"])
    return {"download_url": completed["downloadUrl"]}

Configuration

client = Rynko(
    # Required: Your API key
    api_key="your_api_key",

    # Optional: Custom base URL (default: https://api.rynko.dev)
    base_url="https://api.rynko.dev",

    # Optional: Request timeout in seconds (default: 30)
    timeout=30.0,

    # Optional: Custom headers for all requests
    headers={"X-Custom-Header": "value"},
)

Environment Variables

We recommend using environment variables for configuration:

# .env
RYNKO_API_KEY=your_api_key_here
WEBHOOK_SECRET=your_webhook_secret_here
import os
from dotenv import load_dotenv
from rynko import Rynko

load_dotenv()

client = Rynko(api_key=os.environ["RYNKO_API_KEY"])

Error Handling

from rynko import Rynko, RynkoError

client = Rynko(api_key="your_api_key")

try:
    job = client.documents.generate_pdf(
        template_id="invalid_template",
        variables={},
    )
except RynkoError as e:
    print(f"API Error: {e.message}")
    print(f"Error Code: {e.code}")
    print(f"Status Code: {e.status_code}")

    # Handle specific error codes
    if e.code == "ERR_TMPL_001":
        print("Template not found")
    elif e.code == "ERR_TMPL_003":
        print("Template validation failed")
    elif e.code == "ERR_QUOTA_001":
        print("Document quota exceeded - upgrade your plan")
    elif e.code == "ERR_AUTH_001":
        print("Invalid API key")
    elif e.code == "ERR_AUTH_004":
        print("API key expired or revoked")

Common Error Codes

Code Description
ERR_AUTH_001 Invalid credentials / API key
ERR_AUTH_004 Token expired or revoked
ERR_TMPL_001 Template not found
ERR_TMPL_003 Template validation failed
ERR_DOC_001 Document job not found
ERR_DOC_004 Document generation failed
ERR_QUOTA_001 Document quota exceeded
ERR_QUOTA_002 Rate limit exceeded

Context Manager

Use the client as a context manager to ensure proper resource cleanup:

# Synchronous
with Rynko(api_key="your_api_key") as client:
    job = client.documents.generate_pdf(
        template_id="tmpl_invoice",
        variables={"invoiceNumber": "INV-001"},
    )
    completed = client.documents.wait_for_completion(job["jobId"])
    print(f"Download URL: {completed['downloadUrl']}")

# Asynchronous
async with AsyncRynko(api_key="your_api_key") as client:
    job = await client.documents.generate_pdf(
        template_id="tmpl_invoice",
        variables={"invoiceNumber": "INV-001"},
    )
    completed = await client.documents.wait_for_completion(job["jobId"])
    print(f"Download URL: {completed['downloadUrl']}")

API Reference

Client Methods

Method Returns Description
me() Dict[str, Any] Get current authenticated user
verify_api_key() bool Verify API key is valid

Documents Resource

Method Returns Description
generate(...) Dict[str, Any] Generate a document (PDF, Excel, or CSV)
generate_pdf(...) Dict[str, Any] Generate a PDF document
generate_excel(...) Dict[str, Any] Generate an Excel document
generate_batch(...) Dict[str, Any] Generate multiple documents
get_job(job_id) Dict[str, Any] Get document job by ID
list_jobs(...) Dict[str, Any] List/search document jobs
wait_for_completion(job_id, ...) Dict[str, Any] Poll until job completes or fails

Templates Resource

Method Returns Description
get(template_id) Dict[str, Any] Get template by ID (UUID, shortId, or slug)
list(...) Dict[str, Any] List all templates
list_pdf(...) Dict[str, Any] List PDF templates only
list_excel(...) Dict[str, Any] List Excel templates only

Webhooks Resource

Method Returns Description
get(webhook_id) Dict[str, Any] Get webhook subscription by ID
list() Dict[str, Any] List all webhook subscriptions

Utilities

Function Returns Description
verify_webhook_signature(...) Dict[str, Any] Verify signature and parse webhook event

Examples

See the examples/ directory for runnable code samples:

For complete project templates with full setup, see the developer-resources repository.

Requirements

  • Python 3.8+
  • httpx 0.24+

License

MIT

Support

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

rynko-1.1.0.tar.gz (25.0 kB view details)

Uploaded Source

Built Distribution

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

rynko-1.1.0-py3-none-any.whl (19.0 kB view details)

Uploaded Python 3

File details

Details for the file rynko-1.1.0.tar.gz.

File metadata

  • Download URL: rynko-1.1.0.tar.gz
  • Upload date:
  • Size: 25.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.2

File hashes

Hashes for rynko-1.1.0.tar.gz
Algorithm Hash digest
SHA256 e56258eda55e524ca4cdc3df9eb1bb6d84dc5f7b7c60a67157194b9055881623
MD5 47a31425301aa532eae47448374f130f
BLAKE2b-256 ee3723a144a2b0ddb59fb032d6866cd470b7e3069eb07d902e803d0b138514bf

See more details on using hashes here.

File details

Details for the file rynko-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: rynko-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 19.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.2

File hashes

Hashes for rynko-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d71cf4b247ba6e3ac87c4ab3573390c9b977162dbaf22319f874f9988ee01882
MD5 7e7cb5a0b70bf79096a79bc63d97f37b
BLAKE2b-256 f6214c5e0508487b6c97c0d1a9445d99c7ab63db450edf18f74b8b204d00e544

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