Anleitung zur Freigado API
Stand: 29.10.2026 · Basis-URL: https://freigado.de
Diese Anleitung führt Sie von der Schlüsselerstellung bis zum automatisch abgeholten, unterschriebenen PDF. Alle Codebeispiele sind vollständig und lauffähig – Sie müssen nur Ihren API-Key, das Signing-Secret und Ihre Vorlagen-ID einsetzen.
1. Überblick & Voraussetzungen
Mit der Freigado API steuern Sie Freigaben und Unterschriften direkt aus Ihren eigenen Systemen (DMS, ERP, CRM, Fachanwendung):
- Vorgänge anstoßen – aus einer im Portal gepflegten Vorlage (auch mit mehreren Unterzeichnern und vorausgefüllten Feldern) oder per PDF-Upload.
- Status abfragen – wer wurde eingeladen, wer hat geöffnet, bestätigt oder abgelehnt; mit vollständiger Ereignisliste.
- Ergebnis abholen – das finale, versiegelte PDF inklusive Prüfnachweis direkt in Ihr System übernehmen.
Voraussetzungen
- Tarif Business oder höher (auch während der kostenlosen Testphase). In kleineren Tarifen antwortet die API mit
402 UPGRADE_REQUIRED. - Der Inhaber Ihrer Organisation legt im Portal unter API-Zugang einen Schlüssel an. Sie erhalten zwei Werte:
- API-Key (Form
ipa_live_xxxxxxxx.<geheim>) – identifiziert Ihre Anwendung. - Signing-Secret – signiert jede Anfrage.
- API-Key (Form
- Beide Werte werden nur einmal angezeigt. Speichern Sie sie sofort an einem sicheren Ort Ihrer Server-Anwendung (z. B. Secret-Store, geschützte Konfiguration) – nie im Browser, in einer App für Endgeräte oder in einem Code-Repository.
- Geht ein Schlüssel verloren oder wurde er möglicherweise offengelegt: unter „API-Zugang“ widerrufen und einen neuen anlegen. Widerrufene Schlüssel werden sofort abgelehnt. Tipp: je Anwendung bzw. Umgebung (Test, Produktion) einen eigenen Schlüssel.
2. Schnellstart in fünf Schritten
- Im Portal unter Vorlagen eine Vorlage anlegen (PDF, Rollen der Unterzeichner, Felder).
- Unter API-Zugang einen Schlüssel anlegen und API-Key + Signing-Secret sicher hinterlegen.
GET /api/v1/templatesaufrufen und dieidIhrer Vorlage notieren (plussignerCountundsenderFields).POST /api/v1/documentsmittemplateId, Ihrer eigenen Vorgangsnummer (externalId) und den Empfängern – Freigado verschickt die Einladung.- Regelmäßig
GET /api/v1/documents/{id}abfragen; sobaldresultgefüllt ist, PDF laden, prüfen, speichern und mitPOST …/files/ackbestätigen.
3. Authentifizierung (Signatur)
Jede Anfrage – auch GET – trägt drei Header:
Authorization: Bearer <API-Key>
X-Timestamp: <aktuelle Unix-Zeit in Sekunden oder Millisekunden>
X-Signature: base64url( HMAC-SHA256( <Signing-Secret>, "<X-Timestamp>." + <Body> ) )Signatur Schritt für Schritt
- Zeitstempel bilden, z. B.
1767225600(Sekunden) – Millisekunden sind ebenfalls erlaubt. Genau dieser Text kommt in den HeaderX-Timestamp. - Den Request-Body vollständig als Bytes erzeugen – genau die Bytes, die Sie anschließend senden. Bei GET und anderen Anfragen ohne Body ist er leer.
- Signiert wird die Byte-Folge
Zeitstempel + "." + Body. Bei leerem Body also nur"1767225600."(mit Punkt!). - HMAC-SHA256 berechnen. Schlüssel ist das Signing-Secret als Text (UTF-8-Bytes) – nicht base64-dekodieren, obwohl es so aussieht.
- Ergebnis base64url kodieren:
+→-,/→_, abschließende=entfernen.
Wichtige Details
- Zeitfenster ±300 Sekunden: Weicht die Uhr Ihres Servers stärker ab, antwortet Freigado mit
401 STALE_TIMESTAMP. Halten Sie die Systemzeit per NTP synchron und bilden Sie Zeitstempel und Signatur bei jedem (auch wiederholten) Aufruf neu. - Pfad und Query-Parameter werden nicht signiert – nur Zeitstempel und Body.
- Nach dem Signieren nichts mehr am Body ändern (kein erneutes Serialisieren, keine andere Zeichenkodierung, keine Zeilenumbrüche). Am sichersten: Body einmal als Byte-Array erzeugen, signieren und genau dieses Array senden.
- PDF-Upload (multipart): Viele HTTP-Bibliotheken erzeugen den multipart-Body erst beim Senden (z. B.
curl -F). Das funktioniert nicht, weil die Signatur den fertigen Body braucht. Die Beispiele in Abschnitt 5 zeigen, wie Sie den Body vorab erzeugen.
Testvektor zum Prüfen Ihrer Implementierung
Mit diesen Eingaben muss Ihre Signaturfunktion exakt die angegebenen Werte liefern (alle Beispiele aus Abschnitt 5 wurden damit geprüft):
| Eingabe | Erwartete X-Signature |
|---|---|
Secret test-signing-secret, Timestamp 1767225600, Body leer (GET) | 31aWg89NR2oZ62g_h4pf34j-_jtPimvGliQe7lw_RZM |
wie oben, Body {"externalId":"TEST-1"} | 18pq22pfbOyQELJU4Tqwg07NBiCdjSnFHmLIwz7DS9M |
4. Endpunkte
| Methode & Pfad | Zweck | Erfolg |
|---|---|---|
GET /api/v1/templates | Vorlagen der Organisation auflisten | 200 |
GET /api/v1/templates/{id} | Eine Vorlage abrufen | 200 |
POST /api/v1/documents | Vorgang aus Vorlage (JSON) oder per PDF-Upload (multipart) anlegen | 201 (JSON-Weg: 200 bei Wiederholung) |
GET /api/v1/documents/{id} | Status eines Vorgangs | 200 |
GET /api/v1/documents?externalId=… | Status über Ihre eigene Vorgangsnummer (jüngster Vorgang) | 200 |
GET /api/v1/documents/{id}/files/document | Finales PDF herunterladen | 200 (application/pdf) |
GET /api/v1/documents/{id}/files/attestation | Separates Nachweis-PDF (nur falls vorhanden) | 200 (application/pdf) |
POST /api/v1/documents/{id}/files/ack | Abruf bestätigen – Ergebnisdateien werden sofort gelöscht | 204 |
POST /api/v1/documents/{id}/cancel | Offenen Vorgang zurückziehen | 200 |
GET /api/v1/account | Tarif, Kontingent und freigeschaltete Funktionen | 200 |
4.1 Vorlagen auflisten und abrufen
GET /api/v1/templates liefert alle Vorlagen Ihrer Organisation, GET /api/v1/templates/{id} eine einzelne (ohne Hülle; 404, wenn unbekannt).
{
"templates": [
{
"id": "6f1c2a8e-…",
"name": "Auftragsverarbeitungsvertrag",
"filename": "avv.pdf",
"roles": ["Kunde", "Geschäftsführung"],
"signerCount": 2,
"senderFields": [
{ "id": "firma", "label": "Firma", "type": "text", "required": true }
],
"reminderDays": [3, 7, 14],
"message": "Bitte prüfen und unterschreiben.",
"updatedAt": "2026-10-29T13:12:17.885Z"
}
]
}roles– Reihenfolge der Unterzeichner;signerCount= Anzahl der Rollen = Anzahl Empfänger, die Sie beim Anlegen angeben müssen.senderFields– Felder, die Sie beim Anlegen ausfüllen (Typtext,multilineoderdate). Automatische Datumsfelder (Versanddatum) setzt Freigado selbst; sie erscheinen hier nicht.reminderDays– Erinnerungsplan der Vorlage,message– Standardnachricht an die Empfänger (odernull).
4.2 Vorgang aus Vorlage anlegen (JSON)
POST /api/v1/documents mit Content-Type: application/json:
{
"templateId": "6f1c2a8e-…",
"externalId": "AUFTRAG-2026-0815",
"recipients": [
{ "email": "einkauf@kunde.de" },
{ "email": "gf@kunde.de" }
],
"fieldValues": { "firma": "Muster GmbH" },
"message": "Bitte bis Freitag unterschreiben.",
"reminderDays": [3, 7, 14],
"senderEmail": "vertrieb@ihre-firma.de"
}| Feld | Pflicht | Bedeutung |
|---|---|---|
templateId | ja | ID aus GET /api/v1/templates. |
externalId | ja | Ihre eigene, eindeutige Vorgangsnummer (max. 128 Zeichen). Dient der Zuordnung und der Idempotenz (siehe unten). |
recipients | ja | Liste mit genau signerCount Einträgen { email } in der Reihenfolge der Rollen. Jede Adresse nur einmal. |
fieldValues | je nach Vorlage | Werte für senderFields als { "feld-id": "Wert" }. Pflichtfelder müssen gefüllt sein; unbekannte IDs werden abgelehnt. Datumsfelder als JJJJ-MM-TT oder TT.MM.JJJJ. |
message | nein | Nachricht an die Empfänger (max. 2000 Zeichen). Fehlt das Feld, gilt die Vorlagen-Nachricht; null = keine Nachricht. |
reminderDays | nein | Erinnerungsplan, z. B. [3, 7, 14] = Tage nach Einladung. Ganze Zahlen 1–60, max. 5 Stufen. [] = keine Erinnerungen; fehlt das Feld, gilt der Plan der Vorlage. |
senderEmail | empfohlen | Ihre Absender- bzw. Antwortadresse (z. B. Sachbearbeiter). An sie geht auch die Abschlussbestätigung. |
Antwort 201 Created mit dem vollständigen Statusobjekt (siehe 4.4). Bei mehreren Empfängern unterschreiben diese nacheinander in der Reihenfolge der Rollen.
externalId) erneut, solange der Vorgang noch offen ist, erhalten Sie 200 OK mit dem bestehenden Vorgang – es entsteht kein Duplikat. So können Sie bei Netzwerkfehlern gefahrlos wiederholen. Ist der frühere Vorgang bereits abgeschlossen oder abgelaufen, wird ein neuer angelegt (201).4.3 Vorgang per PDF-Upload anlegen (multipart)
Ohne Vorlage: POST /api/v1/documents mit Content-Type: multipart/form-data – für einen Empfänger mit recipientEmail oder für mehrere nacheinander mit recipients (Details unter 4.6).
| Formularfeld | Pflicht | Bedeutung |
|---|---|---|
file | ja | Das PDF (max. 20 MB). |
recipientEmail | ja* | E-Mail-Adresse des Empfängers (ein Empfänger). |
recipients | ja* | Stattdessen: mehrere Empfänger nacheinander als JSON-Text, z. B. ["x@x.de","y@x.de"] (max. 10, ab Team-Tarif). |
message | nein | Nachricht an die Empfänger (max. 2.000 Zeichen). |
reminderDays | nein | JSON-Array mit Erinnerungstagen. Ohne Angabe [3, 7, 14]; [] schaltet Erinnerungen ab. |
proofDelivery | nein | Nachweis: page (letzte Seite, Standard), embed (eingebettet) oder email (separates PDF). |
attachments | nein | Bis zu 5 Anlagen (PDF); das Feld je Anlage wiederholen. |
attachmentDelivery | nein | Anlagen im Ergebnis: separate (Einzeldateien, Standard), append (Seiten anhängen) oder embed (einbetten). |
externalId | empfohlen | Ihre Vorgangsnummer (max. 128 Zeichen). |
senderEmail | empfohlen | Absender-/Antwortadresse, erhält die Abschlussbestätigung. |
decisionOptions | nein | JSON-Text mit 1–3 eigenen Entscheidungs-Buttons (siehe unten). Ohne Angabe: „Freigeben“ / „Ablehnen“. |
* Genau eines von recipientEmail oder recipients angeben.
[
{ "id": "ok", "label": "Freigegeben", "outcome": "APPROVED" },
{ "id": "auflage", "label": "Mit Auflagen freigegeben", "outcome": "APPROVED",
"annotation": "Freigabe unter Vorbehalt der genannten Änderungen." },
{ "id": "nein", "label": "Abgelehnt", "outcome": "REJECTED" }
]{
"documentId": "b3994c73-…",
"externalId": "VORGANG-4712",
"token": "…",
"reviewUrl": "https://freigado.de/d/…",
"status": "PENDING",
"expiresAt": "2026-11-28T14:17:12.742Z",
"flowMode": "single",
"signers": [ { "position": 0, "email": "pruefer@example.com", "status": "ACTIVE" } ]
}Diese Antwort ist bewusst kompakter als beim JSON-Weg; den vollständigen Status liefert GET /api/v1/documents/{id}. Idempotenz: Existiert für Ihren Schlüssel bereits ein Vorgang mit derselben externalId, wird dieser zurückgegeben (Kennzeichen "idempotent": true). reviewUrl ist der persönliche Link des Empfängers – nicht veröffentlichen.
4.4 Status abfragen
GET /api/v1/documents/{id} oder GET /api/v1/documents?externalId=AUFTRAG-2026-0815:
{
"documentId": "510b1aa7-…",
"externalId": "AUFTRAG-2026-0815",
"status": "APPROVED",
"filename": "avv.pdf",
"createdAt": "2026-10-29T14:17:47.005Z",
"expiresAt": "2026-11-28T14:17:46.995Z",
"reviewUrl": "https://freigado.de/d/…",
"decision": {
"outcome": "APPROVED",
"selectedOptionId": null,
"selectedLabel": null,
"annotation": null,
"comment": "Passt so.",
"decidedAt": "2026-10-30T09:12:03.000Z",
"decidedByEmail": "gf@kunde.de",
"ipAddress": "…",
"userAgent": "…"
},
"templateId": "6f1c2a8e-…",
"signers": [
{ "position": 0, "role": "Kunde", "email": "einkauf@kunde.de", "status": "COMPLETED",
"invitedAt": "…", "firstOpenedAt": "…", "decidedAt": "…", "remindersSent": 1 },
{ "position": 1, "role": "Geschäftsführung", "email": "gf@kunde.de", "status": "COMPLETED",
"invitedAt": "…", "firstOpenedAt": "…", "decidedAt": "…", "remindersSent": 0 }
],
"reminderDays": [3, 7, 14],
"events": [
{ "id": "04bbad8b-…", "type": "created", "at": "…", "signerPosition": null, "data": {} },
{ "id": "811fae85-…", "type": "invited", "at": "…", "signerPosition": 0, "data": {} },
{ "id": "9a0d…", "type": "reminder_sent", "at": "…", "signerPosition": 0,
"data": { "number": 1, "isLast": false } },
…
{ "id": "c71e…", "type": "completed", "at": "…", "signerPosition": null, "data": {} }
],
"result": {
"available": true,
"availableUntil": "2026-11-06T09:12:04.000Z",
"files": [
{ "kind": "document", "filename": "avv.pdf", "size": 13250, "sha256": "5f2c…" }
]
}
}| Feld | Werte / Bedeutung |
|---|---|
status | PENDING offen · APPROVED bestätigt · REJECTED abgelehnt · EXPIRED abgelaufen · CANCELED zurückgezogen |
signers[].status | WAITING noch nicht an der Reihe · ACTIVE eingeladen · COMPLETED bestätigt · REJECTED abgelehnt · CANCELED entfallen (z. B. nach Ablehnung oder Ablauf) |
decision | Ergebnis der Entscheidung (bei mehreren Unterzeichnern: der ablehnenden bzw. zuletzt bestätigenden Person), sonst null. selectedLabel/annotation nur bei eigenen Buttons. |
result | Abholbare Ergebnisdateien mit Größe und SHA-256 – null, solange der Vorgang offen ist, und nach Abruf-Bestätigung oder Ablauf der Aufbewahrung. |
Ereignisse (events) sind chronologisch sortiert und haben eine stabile id – ideal, um bereits verarbeitete Ereignisse zu überspringen. signerPosition verweist auf signers[].position bzw. ist null bei Ereignissen, die den ganzen Vorgang betreffen.
| type | Bedeutung | data |
|---|---|---|
created | Vorgang angelegt | – |
invited | Unterzeichner eingeladen (E-Mail versendet) | – |
opened | Link vom Empfänger geöffnet | – |
otp_verified | Empfänger hat sich per E-Mail-Code bestätigt | – |
reminder_sent | Erinnerung versendet | { "number": 1, "isLast": false } |
signer_completed | Unterzeichner hat bestätigt | bei eigenen Buttons: { "selectedOptionId", "selectedLabel", "annotation" } |
rejected | Abgelehnt | { "comment": "…" | null, …Auswahl } |
completed | Vorgang vollständig bestätigt | – |
expired | Vorgang abgelaufen | – |
result_fetched | Ergebnisabruf bestätigt (ack) | – |
canceled | Vorgang zurückgezogen | { "reason": "…" | null } |
Die Ereignisliste enthält keine IP-Adressen oder Browserdaten; diese stehen im Prüfnachweis des PDFs.
4.5 Ergebnisdateien abholen und Abruf bestätigen
Sobald ein API-Vorgang abgeschlossen ist (bestätigt oder abgelehnt), hält Freigado das finale PDF für Sie bereit. Es ist elektronisch versiegelt und enthält die Nachweisseite mit Zeitstrahl und Prüf-Link. Die Dateien sind in result.files aufgeführt – in der Regel nur document; attestation gibt es nur, wenn ein separates Nachweis-PDF erzeugt wurde.
GET /api/v1/documents/{id}/files/document→ PDF-Datei (application/pdf). Der HeaderX-Content-SHA256enthält die Prüfsumme.- Datei dauerhaft in Ihrem System speichern und die SHA-256-Prüfsumme mit
result.files[].sha256vergleichen. - Erst danach
POST /api/v1/documents/{id}/files/ackmit Body{}→204 No Content. Freigado löscht die Ergebnisdateien sofort.
result.availableUntil (7 Tage nach Abschluss) automatisch gelöscht und sind dann nicht wiederherstellbar. Holen Sie sie daher zeitnah ab. Der Aufruf von ack ist wiederholbar (auch danach 204); für einen noch offenen Vorgang liefert er 409 NOT_PENDING, ein Download vor Abschluss oder nach Löschung 404.4.6 PDF mit Freigabeablauf ohne Vorlage
Sie brauchen keine Vorlage, um ein PDF nacheinander von mehreren Personen freigeben zu lassen: Senden Sie das PDF per multipart (4.3) mit recipients statt recipientEmail. Die Reihenfolge der Liste ist die Reihenfolge des Ablaufs – erst nach der Freigabe von Person 1 erhält Person 2 ihren persönlichen Link per E-Mail, und so weiter. Lehnt eine Person ab, endet der Vorgang. Mehrere Empfänger sind ab dem Team-Tarif enthalten.
Eigene Buttons (decisionOptions) gelten für alle Personen gleich: Ein Button mit "outcome": "APPROVED" gibt das Dokument an die nächste Person weiter bzw. schließt ab, REJECTED bricht ab. Mindestens ein Button muss weiterführen. Der gewählte Button-Text und ggf. der Vermerk stehen je Person im Nachweis, in der Ergebnis-Mail und im Status unter signers[].selectedLabel bzw. signers[].annotation.
recipients = ["x@x.de","y@x.de","z@abc.de"]
decisionOptions = [{"id":"ok","label":"Freigeben","outcome":"APPROVED"}]
message = Bitte prüfen und freigeben.
reminderDays = [3,7,14]
externalId = VORGANG-4713
senderEmail = sachbearbeiter@example.com
attachmentDelivery = append
file = vertrag.pdf (PDF)
attachments = anlage.pdf (optional, bis zu 5×)Anlagen nachweisen: Für jede Anlage speichert Freigado beim Hochladen die SHA-256-Prüfsumme. Der Status enthält attachments mit Dateiname, Größe, Prüfsumme und den Lesebestätigungen je Person (readBy); der Nachweis im PDF nennt sie ebenfalls. So können Sie die Anlagen später mit Ihren Originalen abgleichen, obwohl Freigado sie nach Abschluss löscht.
"attachmentDelivery": "append",
"attachments": [
{ "position": 0, "filename": "anlage.pdf", "size": 864,
"sha256": "35228331…227432f5", "readAt": "2026-10-29T15:45:27.272Z",
"readBy": [ { "position": 0, "email": "x@x.de", "readAt": "…" },
{ "position": 1, "email": "y@x.de", "readAt": "…" } ] }
]Anlagen im Ergebnis (attachmentDelivery): separate (Standard) liefert die Anlagen als eigene Dateien mit der Bestätigungs-E-Mail; append hängt ihre Seiten hinten an das finale PDF an (der Nachweis bleibt die letzte Seite); embed bettet sie als Dateianhänge ein. Bei append und embed geschieht das vor dem Versiegeln – das Siegel deckt die Anlagen also mit ab, und sie sind auch im Ergebnis-PDF der API enthalten.
senderEmail: Sie ist Antwortadresse aller E-Mails an die Empfänger und erhält Ergebnis- und Ablaufmeldungen. Codebeispiele in allen Sprachen finden Sie in Abschnitt 5.4.7 Vorgang zurückziehen
POST /api/v1/documents/{id}/cancel mit optionalem Body { "reason": "Falsche Version hochgeladen" } (max. 500 Zeichen; ein leerer Body ist erlaubt). Nur offene Vorgänge lassen sich zurückziehen, sonst 409 NOT_PENDING.
- Status wird
CANCELED, alle Links werden sofort ungültig. - Dokument und Anlagen werden sofort von der Plattform gelöscht.
- Die gerade angefragte Person erhält eine kurze Info-Mail (mit Begründung, falls angegeben).
- Antwort
200= Statusobjekt mit Ereigniscanceled. Der Vorgang zählt weiterhin im Kontingent.
4.8 Konto und Kontingent abfragen
GET /api/v1/account zeigt vor dem Senden, was Ihr Tarif erlaubt und wie viele Vorgänge im laufenden Zeitraum noch frei sind:
{
"organization": { "id": "…", "name": "Muster GmbH" },
"plan": { "key": "business", "label": "Business", "trial": false, "trialEndsAt": null },
"quota": { "period": "month", "periodStart": "2026-10-01T00:00:00.000Z",
"limit": 1500, "used": 12, "remaining": 1488 },
"features": { "multipleRecipients": true, "maxRecipients": 10, "signature": true,
"maxAttachments": 5, "reminders": true, "templates": true, "decisionOptions": true },
"limits": { "maxUploadMb": 20, "maxOrderMb": 12, "documentTtlDays": 30, "apiResultRetentionDays": 7 },
"apiKey": { "scopes": ["ingest", "read"] }
}limit und remaining sind null bei unbegrenztem Kontingent; period ist trial, solange die Testphase läuft.
5. Vollständige Codebeispiele
Jedes Beispiel zeigt den kompletten Weg: Vorlagen auflisten → Vorgang aus Vorlage anlegen → Status abfragen → Ergebnis laden, prüfen und bestätigen. Darunter jeweils der PDF-Upload ohne Vorlage, der Freigabeablauf mit mehreren Empfängern (inkl. Kontoprüfung) und das Zurückziehen eines Vorgangs. Zugangsdaten werden aus Umgebungsvariablen gelesen (FREIGADO_API_KEY, FREIGADO_SIGNING_SECRET, optional FREIGADO_BASE_URL) – so landen keine Geheimnisse im Code. Ersetzen Sie <vorlagen-id>, die Empfänger-Adressen und fieldValues passend zu Ihrer Vorlage.
5.1 curl / bash
#!/usr/bin/env bash
# Freigado API – Beispiel für bash (benötigt: curl ≥ 7.76, openssl, jq, sha256sum)
set -euo pipefail
BASE_URL="${FREIGADO_BASE_URL:-https://freigado.de}"
API_KEY="${FREIGADO_API_KEY:?Bitte FREIGADO_API_KEY setzen}"
SIGNING_SECRET="${FREIGADO_SIGNING_SECRET:?Bitte FREIGADO_SIGNING_SECRET setzen}"
# Signatur = base64url(HMAC-SHA256(Signing-Secret, "<timestamp>." + Body))
# $1 = Timestamp, $2 = Datei mit dem exakten Body (bei GET weglassen)
sign() {
{ printf '%s.' "$1"; if [ -n "${2:-}" ]; then cat "$2"; fi; } |
openssl dgst -sha256 -hmac "$SIGNING_SECRET" -binary |
openssl base64 -A | tr '+/' '-_' | tr -d '='
}
# api METHODE PFAD [BODY-DATEI] [CONTENT-TYPE] → Antwort auf stdout
api() {
local method="$1" path="$2" body="${3:-}" ctype="${4:-application/json}"
local ts; ts="$(date +%s)" # Unix-Sekunden (Millisekunden sind ebenfalls erlaubt)
local args=(-sS --fail-with-body -X "$method" "$BASE_URL$path"
-H "Authorization: Bearer $API_KEY"
-H "X-Timestamp: $ts"
-H "X-Signature: $(sign "$ts" "$body")")
if [ -n "$body" ]; then args+=(-H "Content-Type: $ctype" --data-binary "@$body"); fi
curl "${args[@]}"
}
# 1) Vorlagen auflisten
api GET /api/v1/templates | jq '.templates[] | {id, name, signerCount, senderFields}'
# 2) Vorgang aus Vorlage anlegen – der Body wird als Datei geschrieben,
# damit exakt dieselben Bytes signiert und gesendet werden.
jq -n '{
templateId: "<vorlagen-id>",
externalId: "VORGANG-4711",
recipients: [ { email: "kunde@example.com" } ],
fieldValues: { },
reminderDays: [3, 7, 14]
}' > body.json
DOC_ID=$(api POST /api/v1/documents body.json | jq -r .documentId)
echo "Vorgang angelegt: $DOC_ID"
# 3) Status abfragen (Polling)
STATUS=$(api GET "/api/v1/documents/$DOC_ID")
echo "$STATUS" | jq '{status, signers: [.signers[] | {email, status}], events: (.events | length)}'
# 4) Ergebnis abholen, SHA-256 prüfen, Abruf bestätigen
if [ "$(echo "$STATUS" | jq -r '.result.available // false')" = "true" ]; then
api GET "/api/v1/documents/$DOC_ID/files/document" > ergebnis.pdf
EXPECTED=$(echo "$STATUS" | jq -r '.result.files[] | select(.kind == "document") | .sha256')
ACTUAL=$(sha256sum ergebnis.pdf | cut -d' ' -f1)
if [ "$EXPECTED" = "$ACTUAL" ]; then
echo '{}' > ack.json
api POST "/api/v1/documents/$DOC_ID/files/ack" ack.json
echo "Ergebnis gespeichert und Abruf bestätigt."
else
echo "Prüfsumme stimmt nicht – Download wiederholen, KEIN ack senden." >&2
fi
fi# Vorgang per PDF-Upload (multipart/form-data) – nutzt sign() und api() von oben.
# curl -F kann nicht verwendet werden: Die Signatur braucht den fertigen Body vorab.
BOUNDARY="----freigado$(date +%s)$RANDOM"
{
printf -- '--%s\r\nContent-Disposition: form-data; name="recipientEmail"\r\n\r\n%s\r\n' "$BOUNDARY" "pruefer@example.com"
printf -- '--%s\r\nContent-Disposition: form-data; name="externalId"\r\n\r\n%s\r\n' "$BOUNDARY" "VORGANG-4712"
printf -- '--%s\r\nContent-Disposition: form-data; name="file"; filename="vertrag.pdf"\r\n' "$BOUNDARY"
printf -- 'Content-Type: application/pdf\r\n\r\n'
cat vertrag.pdf
printf -- '\r\n--%s--\r\n' "$BOUNDARY"
} > upload.bin
api POST /api/v1/documents upload.bin "multipart/form-data; boundary=$BOUNDARY" | jq# PDF mit Freigabeablauf ohne Vorlage – nutzt sign() und api() von oben.
# x@x.de → y@x.de → z@abc.de nacheinander, für alle derselbe Button „Freigeben“.
# 0) Konto prüfen: Kontingent und ob mehrere Empfänger im Tarif enthalten sind
ACCOUNT=$(api GET /api/v1/account)
echo "Noch frei: $(echo "$ACCOUNT" | jq -r '.quota.remaining') Vorgänge"
if [ "$(echo "$ACCOUNT" | jq -r '.features.multipleRecipients')" != "true" ]; then
echo "Mehrere Empfänger sind erst ab dem Team-Tarif verfügbar." >&2; exit 1
fi
# 1) multipart vollständig erzeugen (Textfelder, PDF, optionale Anlage), dann signiert senden
BOUNDARY="----freigado$(date +%s)$RANDOM"
field() { printf -- '--%s\r\nContent-Disposition: form-data; name="%s"\r\n\r\n%s\r\n' "$BOUNDARY" "$1" "$2"; }
pdf() {
printf -- '--%s\r\nContent-Disposition: form-data; name="%s"; filename="%s"\r\n' "$BOUNDARY" "$1" "$(basename "$2")"
printf -- 'Content-Type: application/pdf\r\n\r\n'
cat "$2"
printf -- '\r\n'
}
{
field recipients '["x@x.de","y@x.de","z@abc.de"]'
field decisionOptions '[{"id":"ok","label":"Freigeben","outcome":"APPROVED"}]'
field message 'Bitte prüfen und freigeben.'
field reminderDays '[3,7,14]'
field externalId 'VORGANG-4713'
field senderEmail 'sachbearbeiter@example.com'
field attachmentDelivery 'append' # Anlagen ins Ergebnis: separate (Standard) | append | embed
pdf file vertrag.pdf
pdf attachments anlage.pdf # optional, bis zu 5 Anlagen
printf -- '--%s--\r\n' "$BOUNDARY"
} > ablauf.bin
DOC_ID=$(api POST /api/v1/documents ablauf.bin "multipart/form-data; boundary=$BOUNDARY" | jq -r '.documentId')
echo "Vorgang angelegt: $DOC_ID"
# 2) Status wie gewohnt abfragen (je Person: signers[].status und signers[].selectedLabel)
api GET "/api/v1/documents/$DOC_ID" | jq -r '.status'# Vorgang zurückziehen (nur solange er offen ist) – Begründung optional
printf '%s' '{"reason":"Falsche Version hochgeladen"}' > cancel.json
api POST "/api/v1/documents/$DOC_ID/cancel" cancel.json | jq -r '.status' # → CANCELED5.2 PowerShell (Windows PowerShell 5.1 und PowerShell 7)
Die Kommentare sind bewusst ohne Umlaute geschrieben, damit die Datei auch ohne BOM in Windows PowerShell 5.1 korrekt gelesen wird. Bei HTTP-Fehlern wirft Invoke-WebRequest eine Ausnahme; die Fehlermeldung von Freigado steht im Antworttext (error, code).
# Freigado API - Beispiel fuer Windows PowerShell 5.1 und PowerShell 7
$ErrorActionPreference = "Stop"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$BaseUrl = if ($env:FREIGADO_BASE_URL) { $env:FREIGADO_BASE_URL } else { "https://freigado.de" }
$ApiKey = $env:FREIGADO_API_KEY # "ipa_live_xxxxxxxx.<geheim>"
$SigningSecret = $env:FREIGADO_SIGNING_SECRET
# Signatur = base64url(HMAC-SHA256(Signing-Secret, "<timestamp>." + Body))
function Get-FreigadoSignature([string]$Timestamp, [byte[]]$Body) {
$hmac = New-Object System.Security.Cryptography.HMACSHA256 (,[Text.Encoding]::UTF8.GetBytes($SigningSecret))
$prefix = [Text.Encoding]::UTF8.GetBytes("$Timestamp.")
$data = New-Object byte[] ($prefix.Length + $Body.Length)
[Array]::Copy($prefix, 0, $data, 0, $prefix.Length)
[Array]::Copy($Body, 0, $data, $prefix.Length, $Body.Length)
$b64 = [Convert]::ToBase64String($hmac.ComputeHash($data))
return $b64.TrimEnd("=").Replace("+", "-").Replace("/", "_")
}
# Signierte Anfrage. -OutFile speichert die Antwort (z. B. PDF) als Datei.
function Invoke-Freigado([string]$Method, [string]$Path, [byte[]]$Body = @(),
[string]$ContentType = "application/json", [string]$OutFile) {
$ts = [string][DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$p = @{
Method = $Method
Uri = "$BaseUrl$Path"
UseBasicParsing = $true
Headers = @{
"Authorization" = "Bearer $ApiKey"
"X-Timestamp" = $ts
"X-Signature" = (Get-FreigadoSignature $ts $Body)
}
}
if ($Body.Length -gt 0) { $p.Body = $Body; $p.ContentType = $ContentType }
if ($OutFile) { $p.OutFile = $OutFile }
$r = Invoke-WebRequest @p
if ($OutFile -or $r.StatusCode -eq 204) { return $null }
# Antwort explizit als UTF-8 lesen (PS 5.1 raet sonst die Kodierung)
$text = [Text.Encoding]::UTF8.GetString($r.RawContentStream.ToArray())
return $text | ConvertFrom-Json
}
function ConvertTo-Utf8Json($Object) {
return [Text.Encoding]::UTF8.GetBytes(($Object | ConvertTo-Json -Depth 10 -Compress))
}
# 1) Vorlagen auflisten
$templates = (Invoke-Freigado GET "/api/v1/templates").templates
$templates | Select-Object id, name, signerCount | Format-Table
# 2) Vorgang aus Vorlage anlegen
$body = ConvertTo-Utf8Json @{
templateId = "<vorlagen-id>"
externalId = "VORGANG-4711"
recipients = @(@{ email = "kunde@example.com" })
fieldValues = @{}
reminderDays = @(3, 7, 14)
}
$doc = Invoke-Freigado POST "/api/v1/documents" $body
"Vorgang angelegt: $($doc.documentId)"
# 3) Status abfragen (Polling)
$status = Invoke-Freigado GET "/api/v1/documents/$($doc.documentId)"
"Status: $($status.status), Ereignisse: $(@($status.events).Count)"
# 4) Ergebnis abholen, SHA-256 pruefen, Abruf bestaetigen
if ($status.result -and $status.result.available) {
$file = "$PWD\ergebnis.pdf"
Invoke-Freigado GET "/api/v1/documents/$($doc.documentId)/files/document" -OutFile $file
$expected = ($status.result.files | Where-Object kind -eq "document").sha256
$actual = (Get-FileHash $file -Algorithm SHA256).Hash.ToLower()
if ($expected -eq $actual) {
Invoke-Freigado POST "/api/v1/documents/$($doc.documentId)/files/ack" (ConvertTo-Utf8Json @{}) | Out-Null
"Ergebnis gespeichert und Abruf bestaetigt."
} else {
Write-Warning "Pruefsumme stimmt nicht - Download wiederholen, KEIN ack senden."
}
}# Vorgang per PDF-Upload (multipart/form-data) - nutzt Invoke-Freigado von oben.
$boundary = "----freigado" + [Guid]::NewGuid().ToString("N")
$enc = [Text.Encoding]::UTF8
$ms = New-Object System.IO.MemoryStream
function Add-Part([string]$Text) { $b = $enc.GetBytes($Text); $ms.Write($b, 0, $b.Length) }
Add-Part "--$boundary`r`nContent-Disposition: form-data; name=`"recipientEmail`"`r`n`r`npruefer@example.com`r`n"
Add-Part "--$boundary`r`nContent-Disposition: form-data; name=`"externalId`"`r`n`r`nVORGANG-4712`r`n"
Add-Part "--$boundary`r`nContent-Disposition: form-data; name=`"file`"; filename=`"vertrag.pdf`"`r`nContent-Type: application/pdf`r`n`r`n"
$pdf = [IO.File]::ReadAllBytes("$PWD\vertrag.pdf"); $ms.Write($pdf, 0, $pdf.Length)
Add-Part "`r`n--$boundary--`r`n"
$upload = Invoke-Freigado POST "/api/v1/documents" $ms.ToArray() "multipart/form-data; boundary=$boundary"
"Vorgang angelegt: $($upload.documentId)"# PDF mit Freigabeablauf ohne Vorlage - nutzt Invoke-Freigado von oben.
# x@x.de -> y@x.de -> z@abc.de nacheinander, fuer alle derselbe Button "Freigeben".
# 0) Konto pruefen: Kontingent und ob mehrere Empfaenger im Tarif enthalten sind
$account = Invoke-Freigado GET "/api/v1/account"
"Tarif: $($account.plan.label), noch frei: $($account.quota.remaining)"
if (-not $account.features.multipleRecipients) { throw "Mehrere Empfaenger erst ab dem Team-Tarif." }
# 1) multipart vollstaendig erzeugen (Textfelder, PDF, optionale Anlage), dann signiert senden
$boundary = "----freigado" + [Guid]::NewGuid().ToString("N")
$enc = [Text.Encoding]::UTF8
$ms = New-Object System.IO.MemoryStream
function Add-Part([string]$Text) { $b = $enc.GetBytes($Text); $ms.Write($b, 0, $b.Length) }
function Add-Field([string]$Name, [string]$Value) {
Add-Part "--$boundary`r`nContent-Disposition: form-data; name=`"$Name`"`r`n`r`n$Value`r`n"
}
function Add-Pdf([string]$Name, [string]$Path) {
$fileName = [IO.Path]::GetFileName($Path)
Add-Part "--$boundary`r`nContent-Disposition: form-data; name=`"$Name`"; filename=`"$fileName`"`r`nContent-Type: application/pdf`r`n`r`n"
$bytes = [IO.File]::ReadAllBytes($Path); $ms.Write($bytes, 0, $bytes.Length)
Add-Part "`r`n"
}
Add-Field "recipients" '["x@x.de","y@x.de","z@abc.de"]'
Add-Field "decisionOptions" '[{"id":"ok","label":"Freigeben","outcome":"APPROVED"}]'
Add-Field "message" "Bitte pruefen und freigeben."
Add-Field "reminderDays" "[3,7,14]"
Add-Field "externalId" "VORGANG-4713"
Add-Field "senderEmail" "sachbearbeiter@example.com"
Add-Field "attachmentDelivery" "append" # separate (Standard) | append | embed
Add-Pdf "file" "$PWD\vertrag.pdf"
Add-Pdf "attachments" "$PWD\anlage.pdf" # optional, bis zu 5 Anlagen
Add-Part "--$boundary--`r`n"
$flow = Invoke-Freigado POST "/api/v1/documents" $ms.ToArray() "multipart/form-data; boundary=$boundary"
"Vorgang angelegt: $($flow.documentId) - Ablauf: $(($flow.signers | ForEach-Object { $_.email }) -join ' -> ')"# Vorgang zurueckziehen (nur solange er offen ist) - Begruendung optional
$canceled = Invoke-Freigado POST "/api/v1/documents/$($flow.documentId)/cancel" (ConvertTo-Utf8Json @{ reason = "Falsche Version hochgeladen" })
"Status: $($canceled.status)" # CANCELED5.3 C# / .NET
Die Klasse FreigadoClient kommt ohne Zusatzpakete aus und läuft unter .NET 6+ ebenso wie unter .NET Framework 4.7.2 (dort ggf. TLS 1.2 aktivieren). Das Verwendungsbeispiel nutzt System.Text.Json (.NET 6+).
// Freigado API – Beispiel für C# (.NET 6+; ebenso .NET Framework 4.7.2 mit System.Net.Http)
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
public sealed class FreigadoClient
{
private static readonly HttpClient Http = new HttpClient();
private readonly string _baseUrl;
private readonly string _apiKey;
private readonly string _signingSecret;
public FreigadoClient(string baseUrl, string apiKey, string signingSecret)
{
_baseUrl = baseUrl.TrimEnd('/');
_apiKey = apiKey;
_signingSecret = signingSecret;
}
/// <summary>base64url(HMAC-SHA256(Signing-Secret, "<timestamp>." + Body))</summary>
public static string Sign(string timestamp, byte[] body, string signingSecret)
{
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(signingSecret)))
{
byte[] prefix = Encoding.UTF8.GetBytes(timestamp + ".");
hmac.TransformBlock(prefix, 0, prefix.Length, null, 0);
hmac.TransformFinalBlock(body, 0, body.Length);
return Convert.ToBase64String(hmac.Hash).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
}
/// <summary>Signierte Anfrage. body = exakt die Bytes, die gesendet werden (GET: leer).</summary>
public async Task<HttpResponseMessage> SendAsync(HttpMethod method, string path,
byte[] body = null, string contentType = "application/json")
{
body = body ?? new byte[0];
string ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var req = new HttpRequestMessage(method, _baseUrl + path);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
req.Headers.Add("X-Timestamp", ts);
req.Headers.Add("X-Signature", Sign(ts, body, _signingSecret));
if (body.Length > 0)
{
req.Content = new ByteArrayContent(body);
req.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
}
HttpResponseMessage res = await Http.SendAsync(req);
if (!res.IsSuccessStatusCode)
{
// Fehlerformat: { "error": "...", "code": "...", "meta": { ... } }
string err = await res.Content.ReadAsStringAsync();
throw new HttpRequestException((int)res.StatusCode + ": " + err);
}
return res;
}
public async Task<string> GetJsonAsync(string path)
{
var res = await SendAsync(HttpMethod.Get, path);
return await res.Content.ReadAsStringAsync();
}
public async Task<string> PostJsonAsync(string path, string json)
{
var res = await SendAsync(HttpMethod.Post, path, Encoding.UTF8.GetBytes(json));
return await res.Content.ReadAsStringAsync();
}
/// <summary>Vorgang per PDF-Upload: multipart erst vollständig erzeugen, dann signieren.</summary>
public async Task<string> UploadPdfAsync(string pdfPath, string recipientEmail, string externalId)
{
var form = new MultipartFormDataContent();
var file = new ByteArrayContent(File.ReadAllBytes(pdfPath));
file.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
form.Add(file, "file", Path.GetFileName(pdfPath));
form.Add(new StringContent(recipientEmail), "recipientEmail");
form.Add(new StringContent(externalId), "externalId");
byte[] body = await form.ReadAsByteArrayAsync();
string contentType = form.Headers.ContentType.ToString(); // inkl. boundary
var res = await SendAsync(HttpMethod.Post, "/api/v1/documents", body, contentType);
return await res.Content.ReadAsStringAsync();
}
/// <summary>Ergebnis-PDF laden, SHA-256 prüfen und erst dann den Abruf bestätigen.</summary>
public async Task<bool> DownloadAndAckAsync(string documentId, string expectedSha256, string targetPath)
{
var res = await SendAsync(HttpMethod.Get, "/api/v1/documents/" + documentId + "/files/document");
byte[] pdf = await res.Content.ReadAsByteArrayAsync();
string actual;
using (var sha = SHA256.Create())
actual = BitConverter.ToString(sha.ComputeHash(pdf)).Replace("-", "").ToLowerInvariant();
if (actual != expectedSha256) return false; // kein ack → erneut versuchen
File.WriteAllBytes(targetPath, pdf); // erst sicher speichern …
await SendAsync(HttpMethod.Post, "/api/v1/documents/" + documentId + "/files/ack",
Encoding.UTF8.GetBytes("{}")); // … dann bestätigen (löscht bei Freigado)
return true;
}
}// Verwendung (z. B. in einer Konsolen-App); JSON-Verarbeitung mit System.Text.Json (.NET 6+)
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
public static class Program
{
public static async Task Main()
{
var client = new FreigadoClient(
Environment.GetEnvironmentVariable("FREIGADO_BASE_URL") ?? "https://freigado.de",
Environment.GetEnvironmentVariable("FREIGADO_API_KEY"),
Environment.GetEnvironmentVariable("FREIGADO_SIGNING_SECRET"));
// 1) Vorlagen auflisten
Console.WriteLine(await client.GetJsonAsync("/api/v1/templates"));
// 2) Vorgang aus Vorlage anlegen
string body = JsonSerializer.Serialize(new
{
templateId = "<vorlagen-id>",
externalId = "VORGANG-4711",
recipients = new[] { new { email = "kunde@example.com" } },
fieldValues = new { },
reminderDays = new[] { 3, 7, 14 }
});
using JsonDocument created = JsonDocument.Parse(await client.PostJsonAsync("/api/v1/documents", body));
string id = created.RootElement.GetProperty("documentId").GetString();
Console.WriteLine("Vorgang angelegt: " + id);
// 3) Status abfragen (Polling)
using JsonDocument status = JsonDocument.Parse(await client.GetJsonAsync("/api/v1/documents/" + id));
Console.WriteLine("Status: " + status.RootElement.GetProperty("status").GetString());
// 4) Ergebnis abholen, prüfen, bestätigen
JsonElement result = status.RootElement.GetProperty("result");
if (result.ValueKind == JsonValueKind.Object)
{
foreach (JsonElement f in result.GetProperty("files").EnumerateArray())
{
if (f.GetProperty("kind").GetString() != "document") continue;
bool ok = await client.DownloadAndAckAsync(id, f.GetProperty("sha256").GetString(), "ergebnis.pdf");
Console.WriteLine(ok ? "Ergebnis gespeichert und bestätigt." : "Prüfsumme falsch – erneut versuchen.");
}
}
}
}PDF-Upload ohne Vorlage: await client.UploadPdfAsync("vertrag.pdf", "pruefer@example.com", "VORGANG-4712");
// Ergänzung für die Klasse FreigadoClient (oben): Konto, PDF mit Freigabeablauf, Zurückziehen
public Task<string> GetAccountAsync() => GetJsonAsync("/api/v1/account");
/// <summary>PDF ohne Vorlage an einen oder mehrere Empfänger nacheinander (Reihenfolge = Array).</summary>
public async Task<string> UploadFlowAsync(string pdfPath, string[] recipients, string externalId,
string decisionOptionsJson = null, string message = null, string[] attachmentPaths = null)
{
var form = new MultipartFormDataContent();
var file = new ByteArrayContent(File.ReadAllBytes(pdfPath));
file.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
form.Add(file, "file", Path.GetFileName(pdfPath));
// Empfänger als JSON-Array, z. B. ["x@x.de","y@x.de","z@abc.de"]
form.Add(new StringContent("[\"" + string.Join("\",\"", recipients) + "\"]"), "recipients");
form.Add(new StringContent(externalId), "externalId");
form.Add(new StringContent("[3,7,14]"), "reminderDays");
form.Add(new StringContent("append"), "attachmentDelivery"); // separate (Standard) | append | embed
if (decisionOptionsJson != null) form.Add(new StringContent(decisionOptionsJson), "decisionOptions");
if (message != null) form.Add(new StringContent(message), "message");
foreach (string att in attachmentPaths ?? new string[0]) // optional, bis zu 5 Anlagen
{
var part = new ByteArrayContent(File.ReadAllBytes(att));
part.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
form.Add(part, "attachments", Path.GetFileName(att));
}
byte[] body = await form.ReadAsByteArrayAsync();
var res = await SendAsync(HttpMethod.Post, "/api/v1/documents", body, form.Headers.ContentType.ToString());
return await res.Content.ReadAsStringAsync();
}
/// <summary>Offenen Vorgang zurückziehen (Links ungültig, Datei gelöscht). Antwort = Statusobjekt.</summary>
public Task<string> CancelAsync(string documentId, string reason) =>
PostJsonAsync("/api/v1/documents/" + documentId + "/cancel",
"{\"reason\":\"" + reason.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"}");// Verwendung (z. B. in Program.Main): x@x.de → y@x.de → z@abc.de nacheinander, ein Button „Freigeben“
using JsonDocument account = JsonDocument.Parse(await client.GetAccountAsync());
if (!account.RootElement.GetProperty("features").GetProperty("multipleRecipients").GetBoolean())
throw new InvalidOperationException("Mehrere Empfänger erst ab dem Team-Tarif.");
using JsonDocument flow = JsonDocument.Parse(await client.UploadFlowAsync(
"vertrag.pdf", new[] { "x@x.de", "y@x.de", "z@abc.de" }, "VORGANG-4713",
decisionOptionsJson: "[{\"id\":\"ok\",\"label\":\"Freigeben\",\"outcome\":\"APPROVED\"}]",
message: "Bitte prüfen und freigeben.",
attachmentPaths: new[] { "anlage.pdf" }));
string flowId = flow.RootElement.GetProperty("documentId").GetString();
Console.WriteLine("Vorgang angelegt: " + flowId);
// Bei Bedarf zurückziehen (nur solange offen):
using JsonDocument canceled = JsonDocument.Parse(await client.CancelAsync(flowId, "Falsche Version hochgeladen"));
Console.WriteLine("Status: " + canceled.RootElement.GetProperty("status").GetString()); // CANCELED5.4 Node.js (ab Version 18)
// Freigado API – Beispiel für Node.js 18+ (ohne Zusatzpakete), als freigado.mjs speichern
import crypto from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
const BASE_URL = process.env.FREIGADO_BASE_URL ?? "https://freigado.de";
const API_KEY = process.env.FREIGADO_API_KEY; // "ipa_live_xxxxxxxx.<geheim>"
const SIGNING_SECRET = process.env.FREIGADO_SIGNING_SECRET;
// Signatur = base64url(HMAC-SHA256(Signing-Secret, "<timestamp>." + Body))
function sign(timestamp, body) {
return crypto.createHmac("sha256", SIGNING_SECRET).update(`${timestamp}.`).update(body).digest("base64url");
}
// Signierte Anfrage; body = exakt die Bytes, die gesendet werden (GET: leer)
async function api(method, path, body = Buffer.alloc(0), contentType = "application/json") {
const timestamp = String(Math.floor(Date.now() / 1000));
const headers = {
Authorization: `Bearer ${API_KEY}`,
"X-Timestamp": timestamp,
"X-Signature": sign(timestamp, body),
};
if (body.length > 0) headers["Content-Type"] = contentType;
const res = await fetch(BASE_URL + path, { method, headers, body: body.length > 0 ? body : undefined });
if (!res.ok) {
const err = await res.json().catch(() => ({})); // { error, code, meta }
throw new Error(`${res.status} ${err.code ?? ""} ${err.error ?? res.statusText}`);
}
return res;
}
const jsonBody = (obj) => Buffer.from(JSON.stringify(obj), "utf8");
// 1) Vorlagen auflisten
const { templates } = await (await api("GET", "/api/v1/templates")).json();
console.table(templates.map((t) => ({ id: t.id, name: t.name, signerCount: t.signerCount })));
// 2) Vorgang aus Vorlage anlegen
const doc = await (await api("POST", "/api/v1/documents", jsonBody({
templateId: "<vorlagen-id>",
externalId: "VORGANG-4711",
recipients: [{ email: "kunde@example.com" }],
fieldValues: {},
reminderDays: [3, 7, 14],
}))).json();
console.log("Vorgang angelegt:", doc.documentId);
// 3) Status abfragen (Polling)
const status = await (await api("GET", `/api/v1/documents/${doc.documentId}`)).json();
console.log("Status:", status.status, "– Ereignisse:", status.events.length);
// 4) Ergebnis abholen, SHA-256 prüfen, erst dann Abruf bestätigen
if (status.result?.available) {
const meta = status.result.files.find((f) => f.kind === "document");
const pdf = Buffer.from(await (await api("GET", `/api/v1/documents/${doc.documentId}/files/document`)).arrayBuffer());
if (crypto.createHash("sha256").update(pdf).digest("hex") === meta.sha256) {
await writeFile("ergebnis.pdf", pdf);
await api("POST", `/api/v1/documents/${doc.documentId}/files/ack`, jsonBody({}));
console.log("Ergebnis gespeichert und Abruf bestätigt.");
} else {
console.warn("Prüfsumme stimmt nicht – Download wiederholen, KEIN ack senden.");
}
}// Vorgang per PDF-Upload (multipart/form-data) – nutzt api() von oben.
// Das Formular wird zuerst vollständig kodiert, damit genau diese Bytes signiert werden.
const form = new FormData();
form.append("file", new Blob([await readFile("vertrag.pdf")], { type: "application/pdf" }), "vertrag.pdf");
form.append("recipientEmail", "pruefer@example.com");
form.append("externalId", "VORGANG-4712");
const encoded = new Request("http://localhost/", { method: "POST", body: form });
const uploadBody = Buffer.from(await encoded.arrayBuffer());
const upload = await (await api("POST", "/api/v1/documents", uploadBody, encoded.headers.get("content-type"))).json();
console.log("Vorgang angelegt:", upload.documentId);// PDF mit Freigabeablauf ohne Vorlage – nutzt api() und jsonBody() von oben.
// x@x.de → y@x.de → z@abc.de nacheinander, für alle derselbe Button „Freigeben“.
// 0) Konto prüfen: Kontingent und ob mehrere Empfänger im Tarif enthalten sind
const account = await (await api("GET", "/api/v1/account")).json();
console.log("Tarif:", account.plan.label, "– noch frei:", account.quota.remaining);
if (!account.features.multipleRecipients) throw new Error("Mehrere Empfänger erst ab dem Team-Tarif.");
// 1) Formular vollständig kodieren, dann genau diese Bytes signieren und senden
const flowForm = new FormData();
flowForm.append("file", new Blob([await readFile("vertrag.pdf")], { type: "application/pdf" }), "vertrag.pdf");
flowForm.append("recipients", JSON.stringify(["x@x.de", "y@x.de", "z@abc.de"]));
flowForm.append("decisionOptions", JSON.stringify([{ id: "ok", label: "Freigeben", outcome: "APPROVED" }]));
flowForm.append("message", "Bitte prüfen und freigeben.");
flowForm.append("reminderDays", JSON.stringify([3, 7, 14]));
flowForm.append("externalId", "VORGANG-4713");
flowForm.append("senderEmail", "sachbearbeiter@example.com");
flowForm.append("attachmentDelivery", "append"); // Anlagen ins Ergebnis: separate (Standard) | append | embed
// optional, bis zu 5 Anlagen:
flowForm.append("attachments", new Blob([await readFile("anlage.pdf")], { type: "application/pdf" }), "anlage.pdf");
const flowReq = new Request("http://localhost/", { method: "POST", body: flowForm });
const flowBody = Buffer.from(await flowReq.arrayBuffer());
const flow = await (await api("POST", "/api/v1/documents", flowBody, flowReq.headers.get("content-type"))).json();
console.log("Vorgang angelegt:", flow.documentId, "–", flow.signers.map((s) => s.email).join(" → "));// Vorgang zurückziehen (nur solange er offen ist) – Begründung optional
const canceled = await (await api("POST", `/api/v1/documents/${flow.documentId}/cancel`,
jsonBody({ reason: "Falsche Version hochgeladen" }))).json();
console.log("Status:", canceled.status); // CANCELED5.5 Python (ab 3.8, mit requests)
# Freigado API – Beispiel für Python 3.8+ (pip install requests)
import base64, hashlib, hmac, json, os, time
import requests
BASE_URL = os.environ.get("FREIGADO_BASE_URL", "https://freigado.de")
API_KEY = os.environ["FREIGADO_API_KEY"] # "ipa_live_xxxxxxxx.<geheim>"
SIGNING_SECRET = os.environ["FREIGADO_SIGNING_SECRET"]
def sign(timestamp: str, body: bytes) -> str:
"""base64url(HMAC-SHA256(Signing-Secret, "<timestamp>." + Body)) ohne '='."""
mac = hmac.new(SIGNING_SECRET.encode("utf-8"), timestamp.encode("ascii") + b"." + body, hashlib.sha256)
return base64.urlsafe_b64encode(mac.digest()).rstrip(b"=").decode("ascii")
def api(method: str, path: str, body: bytes = b"", content_type: str = "application/json") -> requests.Response:
"""Signierte Anfrage; body = exakt die Bytes, die gesendet werden (GET: leer)."""
timestamp = str(int(time.time()))
headers = {
"Authorization": f"Bearer {API_KEY}",
"X-Timestamp": timestamp,
"X-Signature": sign(timestamp, body),
}
if body:
headers["Content-Type"] = content_type
res = requests.request(method, BASE_URL + path, headers=headers, data=body or None, timeout=60)
if not res.ok:
raise RuntimeError(f"{res.status_code}: {res.text}") # {"error", "code", "meta"}
return res
def json_body(obj) -> bytes:
return json.dumps(obj, ensure_ascii=False).encode("utf-8")
# 1) Vorlagen auflisten
for t in api("GET", "/api/v1/templates").json()["templates"]:
print(t["id"], t["name"], "Empfänger:", t["signerCount"])
# 2) Vorgang aus Vorlage anlegen
doc = api("POST", "/api/v1/documents", json_body({
"templateId": "<vorlagen-id>",
"externalId": "VORGANG-4711",
"recipients": [{"email": "kunde@example.com"}],
"fieldValues": {},
"reminderDays": [3, 7, 14],
})).json()
print("Vorgang angelegt:", doc["documentId"])
# 3) Status abfragen (Polling)
status = api("GET", f"/api/v1/documents/{doc['documentId']}").json()
print("Status:", status["status"], "– Ereignisse:", len(status["events"]))
# 4) Ergebnis abholen, SHA-256 prüfen, erst dann Abruf bestätigen
if status.get("result"):
meta = next(f for f in status["result"]["files"] if f["kind"] == "document")
pdf = api("GET", f"/api/v1/documents/{doc['documentId']}/files/document").content
if hashlib.sha256(pdf).hexdigest() == meta["sha256"]:
with open("ergebnis.pdf", "wb") as fh:
fh.write(pdf)
api("POST", f"/api/v1/documents/{doc['documentId']}/files/ack", json_body({}))
print("Ergebnis gespeichert und Abruf bestätigt.")
else:
print("Prüfsumme stimmt nicht – Download wiederholen, KEIN ack senden.")# Vorgang per PDF-Upload (multipart/form-data) – nutzt api() von oben.
# requests kodiert das Formular vorab; signiert werden genau diese Bytes.
with open("vertrag.pdf", "rb") as fh:
prepared = requests.Request(
"POST", BASE_URL,
files={"file": ("vertrag.pdf", fh.read(), "application/pdf")},
data={"recipientEmail": "pruefer@example.com", "externalId": "VORGANG-4712"},
).prepare()
upload = api("POST", "/api/v1/documents", prepared.body, prepared.headers["Content-Type"]).json()
print("Vorgang angelegt:", upload["documentId"])# PDF mit Freigabeablauf ohne Vorlage – nutzt api() und json_body() von oben.
# x@x.de → y@x.de → z@abc.de nacheinander, für alle derselbe Button „Freigeben“.
# 0) Konto prüfen: Kontingent und ob mehrere Empfänger im Tarif enthalten sind
account = api("GET", "/api/v1/account").json()
print("Tarif:", account["plan"]["label"], "– noch frei:", account["quota"]["remaining"])
if not account["features"]["multipleRecipients"]:
raise SystemExit("Mehrere Empfänger erst ab dem Team-Tarif.")
# 1) Formular vorab kodieren; signiert werden genau diese Bytes
with open("vertrag.pdf", "rb") as fh, open("anlage.pdf", "rb") as ah:
prepared = requests.Request(
"POST", BASE_URL,
files=[
("file", ("vertrag.pdf", fh.read(), "application/pdf")),
("attachments", ("anlage.pdf", ah.read(), "application/pdf")), # optional, bis zu 5
],
data={
"recipients": json.dumps(["x@x.de", "y@x.de", "z@abc.de"]),
"decisionOptions": json.dumps([{"id": "ok", "label": "Freigeben", "outcome": "APPROVED"}]),
"message": "Bitte prüfen und freigeben.",
"reminderDays": json.dumps([3, 7, 14]),
"externalId": "VORGANG-4713",
"senderEmail": "sachbearbeiter@example.com",
"attachmentDelivery": "append", # separate (Standard) | append | embed
},
).prepare()
flow = api("POST", "/api/v1/documents", prepared.body, prepared.headers["Content-Type"]).json()
print("Vorgang angelegt:", flow["documentId"], "-", " -> ".join(s["email"] for s in flow["signers"]))# Vorgang zurückziehen (nur solange er offen ist) – Begründung optional
canceled = api("POST", f"/api/v1/documents/{flow['documentId']}/cancel",
json_body({"reason": "Falsche Version hochgeladen"})).json()
print("Status:", canceled["status"]) # CANCELED6. Empfohlener Ablauf in Ihrer Anwendung
Freigado arbeitet mit Polling statt Webhooks: Ihre Anwendung fragt den Status aktiv ab. Dafür genügen ausgehende HTTPS-Verbindungen – das funktioniert auch hinter Firewalls und bei On-Premise-Systemen, ohne dass Sie einen Endpunkt öffnen müssen.
- Anlegen: Vorgang mit Ihrer
externalIdanlegen und diedocumentIdspeichern. Bei Zeitüberschreitung oder Netzwerkfehler denselben Aufruf mit derselbenexternalIdwiederholen (Idempotenz). - Abfragen: offene Vorgänge in einem ruhigen Intervall abfragen – empfohlen alle 5 Minuten (nicht häufiger als einmal pro Minute je Vorgang). Abgeschlossene Vorgänge müssen Sie nur noch bis zum Ergebnisabruf abfragen.
- Ereignisse übernehmen: neue Einträge aus
eventsanhand ihreriderkennen (bereits verarbeitete IDs merken) und z. B. als Notiz oder Statusänderung in Ihrem System ablegen. - Ergebnis sichern: sobald
resultgefüllt ist, PDF laden, SHA-256 prüfen, dauerhaft speichern – und erst dannacksenden. Schlägt etwas fehl, einfach im nächsten Durchlauf erneut versuchen (innerhalb der 7 Tage). - Abschluss: Bei
APPROVED,REJECTEDoderEXPIREDoderCANCELEDist der Vorgang beendet; nach demack(bzw. beiEXPIREDsofort) können Sie das Polling für ihn einstellen.
7. Fehlercodes
Fehler werden einheitlich als JSON geliefert (meta nur, wenn es Details gibt):
{
"error": "Diese Vorlage erwartet genau 2 Empfänger (je Rolle einen), erhalten: 1.",
"code": "VALIDATION",
"meta": { "signerCount": 2, "received": 1 }
}| HTTP | code | Ursache & Abhilfe |
|---|---|---|
| 401 | UNAUTHORIZED_KEY | API-Key fehlt, ist falsch oder widerrufen – oder der Schlüssel hat nicht die nötige Berechtigung. |
| 401 | STALE_TIMESTAMP | X-Timestamp fehlt oder weicht mehr als 300 Sekunden ab. Serverzeit prüfen (NTP). |
| 401 | BAD_SIGNATURE | Signatur passt nicht. Mit dem Testvektor prüfen; Body nach dem Signieren nicht verändern; Secret als Text verwenden. |
| 402 | UPGRADE_REQUIRED | Tarif ohne API-Zugang – Wechsel auf Business oder höher. |
| 402 | QUOTA_EXCEEDED | Monatliches Dokument-Kontingent erreicht (meta: limit, used). |
| 404 | NOT_FOUND | Vorlage, Vorgang oder Ergebnisdatei nicht (mehr) vorhanden – oder mit einem anderen Schlüssel angelegt. |
| 409 | NOT_PENDING | ack für einen noch offenen Vorgang – oder cancel für einen bereits abgeschlossenen. |
| 422 | VALIDATION | Ungültige Eingaben, z. B. falsche Empfängerzahl (meta.signerCount/received), fehlende Pflichtfelder (meta.missingFields), unbekannte Felder (meta.unknownFields), ungültiges JSON oder PDF. |
| 500 | – | Unerwarteter Fehler. Bitte später erneut versuchen; bei Wiederholung den Support kontaktieren. |
8. Erinnerungen, Laufzeit & Kontingent
- Erinnerungen: Freigado erinnert den jeweils aktiven Unterzeichner automatisch nach dem Erinnerungsplan (Tage nach seiner Einladung, Standard 3, 7 und 14 Tage). Je Tag wird höchstens eine Erinnerung versendet; wechselt der Unterzeichner, beginnt die Zählung für ihn neu. Jede Erinnerung erscheint als Ereignis
reminder_sent. - Laufzeit: Ein Vorgang ist 30 Tage gültig (
expiresAt). Wird er bis dahin nicht abgeschlossen, wechselt er aufEXPIREDund die Datei wird gelöscht. Legen Sie bei Bedarf einen neuen Vorgang an. - Kontingent: Jeder angelegte Vorgang zählt wie im Portal gegen das monatliche Dokument-Kontingent Ihres Tarifs (Business: 1.500 Vorgänge pro Monat). Wiederholte, idempotente Aufrufe zählen nicht doppelt. Ist das Kontingent erreicht, antwortet die API mit
402 QUOTA_EXCEEDED. - Benachrichtigungen: Unabhängig von der API erhalten Empfänger und Absender (
senderEmail) nach Abschluss eine Bestätigungsmail.
9. Rechtliche Einordnung
Freigado erzeugt eine einfache elektronische Signatur (EES) mit nachvollziehbarem Prüfnachweis: Der Empfänger bestätigt sich per E-Mail-Code, jeder Schritt wird protokolliert, und das finale PDF ist versiegelt und über einen Prüf-Link verifizierbar. Damit eignet sich Freigado für fast alle Verträge und Freigaben im Geschäftsalltag – etwa Angebote, Aufträge, Auftragsverarbeitungsverträge, Abnahmen oder Bestätigungen.
Für die wenigen Fälle mit gesetzlich vorgeschriebener Schriftform finden Sie eine Übersicht unter Wofür Freigado passt. Diese Hinweise sind keine Rechtsberatung.
10. Datenschutz & Sicherheit
- Datensparsam: Original, Zwischenstände und Anlagen werden nach Abschluss eines Vorgangs sofort gelöscht. Bei API-Vorgängen bleibt nur das Ergebnis-PDF bis zu Ihrem Abruf liegen – höchstens 7 Tage.
- EU-Hosting: Verarbeitung und Speicherung ausschließlich in der EU.
- Schlüssel schützen: API-Key und Signing-Secret nur serverseitig verwenden, nicht protokollieren und regelmäßig bzw. bei Personalwechsel erneuern (neuen Schlüssel anlegen, Anwendung umstellen, alten widerrufen). Unter „API-Zugang“ sehen Sie, wann ein Schlüssel zuletzt benutzt wurde.
- Replay-Schutz: Durch Zeitstempel und Signatur kann eine mitgeschnittene Anfrage nicht verändert und nur innerhalb weniger Minuten wiederholt werden; die Verbindung ist zusätzlich per HTTPS verschlüsselt.
Details finden Sie in der Datenschutzerklärung.
Fragen zur Anbindung?
Schreiben Sie uns an info@it-scom.com – bitte mit Uhrzeit, Endpunkt, HTTP-Status und code der Antwort, aber niemals mit API-Key oder Signing-Secret.