Darstellung
Quickstart: Python
Dieses Beispiel sendet ein PDF an einen Signer, wartet auf die Signatur und speichert das versiegelte PDF. Es braucht nur httpx.
Du brauchst:
- Python 3.10 oder neuer
- einen ratum-Account in einem bezahlten Tarif. API-Zugang gibt es nur mit hinterlegter Zahlungsmethode.
- einen API-Key mit den Scopes
documents:read,documents:writeunddocuments:send(Key erzeugen) - ein PDF mit mindestens einer A4-Seite, z. B.
vertrag.pdf
Zum Ausprobieren: Test-Key
Mit einem Test-Key (ratum_test_…) gehen keine Mails raus und es wird kein Kontingent belastet. Die Signier-Links holst du über den Link-Endpoint. Siehe Test-Modus.
1. Projekt anlegen
sh
mkdir ratum-quickstart && cd ratum-quickstart
python3 -m venv .venv && source .venv/bin/activate
pip install httpx
export RATUM_API_KEY=ratum_live_… # dein Key, nie ins Repository committenLege die Datei quickstart.py mit dem vollständigen Skript unten an. Die folgenden Schritte erklären die einzelnen Teile.
2. Client und Key prüfen
Jeder Request trägt den Key als Bearer-Token. Fehler kommen als application/problem+json; der Helper wirft sie mit ihrem stabilen code weiter:
py
API_URL = os.environ.get("RATUM_API_URL", "https://api.ratum.ch")
API_KEY = os.environ.get("RATUM_API_KEY", "")
client = httpx.Client(
base_url=f"{API_URL}/api/v1",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
)
def ratum(method: str, path: str, **kwargs: Any) -> Any:
response = client.request(method, path, **kwargs)
if response.is_error:
# API-Fehler kommen als application/problem+json mit stabilem `code` (Leitfaden
# "Fehlercodes"). Ein vorgelagerter Proxy (z. B. 502/503) kann aber auch HTML liefern.
if "problem+json" in response.headers.get("content-type", ""):
problem = response.json()
raise RuntimeError(
f"{method} {path} → {response.status_code} {problem.get('code')}: "
f"{problem.get('detail', '')}"
)
raise RuntimeError(f"{method} {path} → {response.status_code}: {response.text[:200]}")
return None if response.status_code == 204 else response.json()GET /api-keys/current zeigt, ob der Key funktioniert, und nennt Konto und Scopes:
py
me = ratum("GET", "/api-keys/current")
print(f"Angemeldet als {me['user_email']} mit {me['key']['prefix']}…")3. Dokument, Signer und Felder in einem Aufruf
POST /documents/create-and-send legt Dokument, Signer und Signaturfelder an und fordert den Versand an. Der eigentliche Versand passiert, sobald die Datei hochgeladen und geprüft ist.
- Koordinaten sind PDF-Punkte (1 pt = 1/72 Zoll), Ursprung oben links.
page_width/page_heightsind die Masse der Seite und dienen der Prüfung, ob das Feld auf die Seite passt. Eine A4-Seite misst 595 × 842 pt. - Statt Koordinaten kannst du Felder auch per Platzhalter im Dokument setzen (
"anchors": {}), siehe Anker. - Der
Idempotency-Keyist hier Pflicht. Bei einem Netzwerkfehler wiederholst du den Request mit demselben Key und bekommst dieselbe Antwort, aber kein zweites Dokument (Idempotenz).
py
data = file_path.read_bytes()
created = ratum(
"POST",
"/documents/create-and-send",
json={
"filename": file_path.name,
"content_type": "application/pdf",
"size_bytes": len(data),
"signers": [
{
"email": signer_email,
"name": "Erika Muster",
"fields": [
# PDF-Punkte, Ursprung oben links. page_width/page_height = Seite (A4).
{
"field_type": "signature",
"page": 1,
"x": 72,
"y": 700,
"width": 200,
"height": 50,
"page_width": 595,
"page_height": 842,
},
{
"field_type": "date",
"page": 1,
"x": 320,
"y": 715,
"width": 120,
"height": 24,
"page_width": 595,
"page_height": 842,
},
],
}
],
"expiry_days": 14,
},
# Pflicht bei createAndSend: ein Retry mit demselben Key legt kein zweites Dokument an.
headers={"Idempotency-Key": str(uuid.uuid4())},
)
document_id = created["document_id"]4. Datei hochladen
Die Antwort enthält eine vorsignierte upload_url. Lade die Datei mit PUT und den mitgelieferten upload_headers hoch, ohne Authorization-Header. Danach meldest du den Upload mit /complete ab:
py
# Die Upload-URL ist vorsigniert: ohne Authorization-Header, aber mit upload_headers.
put = httpx.put(created["upload_url"], content=data, headers=created["upload_headers"])
if put.is_error:
# Nicht raise_for_status(): dessen Meldung enthielte die vorsignierte URL samt Signatur.
# Der Storage antwortet mit XML (z. B. <Code>SignatureDoesNotMatch</Code>).
raise RuntimeError(f"Upload fehlgeschlagen: {put.status_code} {put.text[:300]}")
ratum("POST", f"/documents/{document_id}/complete")
print(f"Dokument {document_id} hochgeladen — Versand folgt nach dem Malware-Scan.")Ab hier läuft alles serverseitig: Malware-Scan, bei DOCX die Konvertierung nach PDF, dann der Versand. Die Einladung geht per E-Mail an den Signer.
5. Status verfolgen
GET /documents/{id} liefert Dokument-Status, Signer-Status und gegebenenfalls einen failure_code. Das Beispiel fragt alle 10 Sekunden ab. In Produktion nimmst du besser Webhooks.
py
done = {"signed", "archived", "declined", "failed"}
deadline = time.monotonic() + 3600 # höchstens 1 h warten
doc = ratum("GET", f"/documents/{document_id}")
while doc["status"] not in done and not any(s["status"] == "expired" for s in doc["signers"]):
if time.monotonic() > deadline:
raise RuntimeError(f"Zeitüberschreitung: Status {doc['status']}")
signers = ", ".join(f"{s['email']}={s['status']}" for s in doc["signers"])
print(f"Status {doc['status']} · Signer: {signers}")
time.sleep(10)
doc = ratum("GET", f"/documents/{document_id}")
if doc["status"] == "failed":
raise RuntimeError(f"Versand gescheitert: {doc['failure_code']}")status | Bedeutung |
|---|---|
draft | angelegt, Upload/Scan läuft noch (send_requested: true = Versand ist angefordert) |
pending | versendet, wartet auf Signaturen |
signed | alle haben signiert; das versiegelte PDF ist Sekunden später abrufbar |
archived | zusätzlich im Langzeitarchiv abgelegt (oder im GUI manuell archiviert) |
declined | ein Signer hat abgelehnt |
failed | Versand gescheitert, Grund in failure_code (z. B. scan_infected) |
6. Versiegeltes PDF laden
Nach der letzten Signatur versiegelt ratum das PDF im Hintergrund. Bis das fertig ist, meist nach wenigen Sekunden, antwortet /download/sealed mit 404. Das Beispiel wartet deshalb kurz.
py
if doc["status"] in {"signed", "archived"}:
# Versiegelt wird kurz nach der letzten Signatur im Hintergrund; bis dahin kommt 404.
for _attempt in range(30):
sealed = client.get(f"/documents/{document_id}/download/sealed")
if sealed.status_code != 404:
break
time.sleep(2)
if sealed.status_code == 404:
raise RuntimeError("PDF nach 60 s noch nicht versiegelt — später erneut abrufen.")
if sealed.is_error:
raise RuntimeError(f"Download fehlgeschlagen: {sealed.status_code} {sealed.text[:200]}")
target = Path(f"{document_id}-signiert.pdf")
target.write_bytes(sealed.content)
print(f"Versiegeltes PDF gespeichert: {target}")
else:
# Abgelehnt oder Frist abgelaufen: kein Erfolg — Exit-Code 1 für Skripte/Cron-Jobs.
states = ", ".join(f"{s['email']}={s['status']}" for s in doc["signers"])
raise RuntimeError(f"Nicht signiert: Status {doc['status']} · Signer: {states}")Ausführen
sh
python quickstart.py vertrag.pdf signer@example.comDie erwartete Ausgabe sieht so aus:
Angemeldet als du@firma.ch mit ratum_live_a1b2c3…
Dokument 6f1c… hochgeladen — Versand folgt nach dem Malware-Scan.
Status draft · Signer: signer@example.com=pending
Status pending · Signer: signer@example.com=pending
… (jetzt im Postfach von signer@example.com signieren)
Versiegeltes PDF gespeichert: 6f1c…-signiert.pdfDas Dokument erscheint auch in deinem ratum-Dashboard, dort mit dem Badge «via API».
Wenn etwas schiefgeht
| Meldung | Ursache und Lösung |
|---|---|
401 unauthenticated | Key fehlt, ist falsch kopiert, abgelaufen oder widerrufen |
403 plan_required | API-Zugang braucht einen bezahlten Tarif; Zahlungsmethode in ratum hinterlegen |
403 insufficient_scope | dem Key fehlt ein Scope; neuen Key mit documents:read, documents:write und documents:send erzeugen |
422 validation_error | Feld ragt über die Seite oder ein Pflichtfeld fehlt; Details stehen in errors |
Versand gescheitert: scan_infected | die Datei wurde als schädlich erkannt und entfernt |
Nicht signiert: Status declined … | ein Signer hat abgelehnt bzw. seine Frist ist abgelaufen; das Skript endet mit Exit-Code 1 |
PDF nach 60 s noch nicht versiegelt | das Versiegeln dauert ungewöhnlich lange; später erneut /download/sealed abrufen |
502/503 mit HTML-Text | kurzzeitige Störung vor der API; nach kurzer Wartezeit erneut versuchen |
Alle Codes stehen unter Fehlercodes.
Mit dem SDK
ratum nimmt dir Upload, Idempotency-Key, Retries bei 429 und transienten 5xx (mit Retry-After, höchstens 30 s Wartezeit) und das Polling ab. Es gibt Ratum (synchron) und AsyncRatum mit denselben Methoden, ab Python 3.11.
bash
pip install ratumpy
"""Quickstart mit dem Python-SDK (``ratum``). Eingebunden in docs.ratum.ch/quickstart/python."""
import os
import time
from pathlib import Path
from ratum import NotFoundError, Ratum
with Ratum(os.environ["RATUM_API_KEY"]) as client:
doc = client.create_and_send(
"vertrag.pdf",
signers=[
{
"email": "erika@example.com",
"name": "Erika Muster",
"fields": [
{
"field_type": "signature",
"page": 1,
"x": 72,
"y": 700,
"width": 200,
"height": 50,
"page_width": 595,
"page_height": 842,
}
],
}
],
expiry_days=14,
)
done = client.wait_for_completion(doc.id, timeout=3600)
if done.status not in {"signed", "archived"}:
raise SystemExit(f"Nicht signiert: {done.status} {done.failure_code or ''}")
# Das Siegel wird kurz nach `signed` geschrieben; bis dahin 404 (NotFoundError).
for attempt in range(30):
try:
Path(f"{doc.id}-signiert.pdf").write_bytes(client.download_sealed_document(doc.id))
break
except NotFoundError:
if attempt == 29:
raise
time.sleep(2)Wichtige Methoden: create_and_send, wait_for_completion, get_document, download_sealed_document, get_signing_link und verify_webhook_signature. API-Fehler sind typisierte Exceptions (RateLimitError, NotFoundError, …) mit dem code aus Fehlercodes. Details: sdk/python/README.md im Repository.
Vollständiges Skript
quickstart.py
py
"""ratum Quickstart (Python 3.10+, nur httpx).
pip install httpx
RATUM_API_KEY=ratum_live_… python quickstart.py vertrag.pdf du@example.com
Legt ein Dokument an, lädt die Datei hoch, lässt es an einen Signer senden, wartet auf die
Signatur und speichert das versiegelte PDF. Die Abschnitte (# region) sind in der Doku
eingebunden: docs.ratum.ch/quickstart/python
"""
import os
import sys
import time
import uuid
from pathlib import Path
from typing import Any
import httpx
# region client
API_URL = os.environ.get("RATUM_API_URL", "https://api.ratum.ch")
API_KEY = os.environ.get("RATUM_API_KEY", "")
client = httpx.Client(
base_url=f"{API_URL}/api/v1",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
)
def ratum(method: str, path: str, **kwargs: Any) -> Any:
response = client.request(method, path, **kwargs)
if response.is_error:
# API-Fehler kommen als application/problem+json mit stabilem `code` (Leitfaden
# "Fehlercodes"). Ein vorgelagerter Proxy (z. B. 502/503) kann aber auch HTML liefern.
if "problem+json" in response.headers.get("content-type", ""):
problem = response.json()
raise RuntimeError(
f"{method} {path} → {response.status_code} {problem.get('code')}: "
f"{problem.get('detail', '')}"
)
raise RuntimeError(f"{method} {path} → {response.status_code}: {response.text[:200]}")
return None if response.status_code == 204 else response.json()
# endregion client
def main(file_path: Path, signer_email: str) -> None:
# region whoami
me = ratum("GET", "/api-keys/current")
print(f"Angemeldet als {me['user_email']} mit {me['key']['prefix']}…")
# endregion whoami
# region create
data = file_path.read_bytes()
created = ratum(
"POST",
"/documents/create-and-send",
json={
"filename": file_path.name,
"content_type": "application/pdf",
"size_bytes": len(data),
"signers": [
{
"email": signer_email,
"name": "Erika Muster",
"fields": [
# PDF-Punkte, Ursprung oben links. page_width/page_height = Seite (A4).
{
"field_type": "signature",
"page": 1,
"x": 72,
"y": 700,
"width": 200,
"height": 50,
"page_width": 595,
"page_height": 842,
},
{
"field_type": "date",
"page": 1,
"x": 320,
"y": 715,
"width": 120,
"height": 24,
"page_width": 595,
"page_height": 842,
},
],
}
],
"expiry_days": 14,
},
# Pflicht bei createAndSend: ein Retry mit demselben Key legt kein zweites Dokument an.
headers={"Idempotency-Key": str(uuid.uuid4())},
)
document_id = created["document_id"]
# endregion create
# region upload
# Die Upload-URL ist vorsigniert: ohne Authorization-Header, aber mit upload_headers.
put = httpx.put(created["upload_url"], content=data, headers=created["upload_headers"])
if put.is_error:
# Nicht raise_for_status(): dessen Meldung enthielte die vorsignierte URL samt Signatur.
# Der Storage antwortet mit XML (z. B. <Code>SignatureDoesNotMatch</Code>).
raise RuntimeError(f"Upload fehlgeschlagen: {put.status_code} {put.text[:300]}")
ratum("POST", f"/documents/{document_id}/complete")
print(f"Dokument {document_id} hochgeladen — Versand folgt nach dem Malware-Scan.")
# endregion upload
# region poll
done = {"signed", "archived", "declined", "failed"}
deadline = time.monotonic() + 3600 # höchstens 1 h warten
doc = ratum("GET", f"/documents/{document_id}")
while doc["status"] not in done and not any(s["status"] == "expired" for s in doc["signers"]):
if time.monotonic() > deadline:
raise RuntimeError(f"Zeitüberschreitung: Status {doc['status']}")
signers = ", ".join(f"{s['email']}={s['status']}" for s in doc["signers"])
print(f"Status {doc['status']} · Signer: {signers}")
time.sleep(10)
doc = ratum("GET", f"/documents/{document_id}")
if doc["status"] == "failed":
raise RuntimeError(f"Versand gescheitert: {doc['failure_code']}")
# endregion poll
# region download
if doc["status"] in {"signed", "archived"}:
# Versiegelt wird kurz nach der letzten Signatur im Hintergrund; bis dahin kommt 404.
for _attempt in range(30):
sealed = client.get(f"/documents/{document_id}/download/sealed")
if sealed.status_code != 404:
break
time.sleep(2)
if sealed.status_code == 404:
raise RuntimeError("PDF nach 60 s noch nicht versiegelt — später erneut abrufen.")
if sealed.is_error:
raise RuntimeError(f"Download fehlgeschlagen: {sealed.status_code} {sealed.text[:200]}")
target = Path(f"{document_id}-signiert.pdf")
target.write_bytes(sealed.content)
print(f"Versiegeltes PDF gespeichert: {target}")
else:
# Abgelehnt oder Frist abgelaufen: kein Erfolg — Exit-Code 1 für Skripte/Cron-Jobs.
states = ", ".join(f"{s['email']}={s['status']}" for s in doc["signers"])
raise RuntimeError(f"Nicht signiert: Status {doc['status']} · Signer: {states}")
# endregion download
if __name__ == "__main__":
if not API_KEY or len(sys.argv) != 3:
sys.exit(
"Aufruf: RATUM_API_KEY=ratum_live_… python quickstart.py <datei.pdf> <signer-email>"
)
try:
main(Path(sys.argv[1]), sys.argv[2])
except (RuntimeError, httpx.HTTPError) as err:
sys.exit(str(err))