API-Dokumentation
Ein Endpunkt, ein Bearer-Token, Formulardaten hinein, JSON heraus. Ein Brief über die API kostet dieselben 2,00 EUR und wird vom Guthaben des Kontos abgezogen.
API-Schlüssel
Jedes Konto hat einen Schlüssel. Melden Sie sich an, um Ihren zu sehen, oder legen Sie ein Konto an.
Guthaben abfragen
Vor einem Serienlauf abfragen, damit ein Auftrag nicht wegen zwei Euro auf halber Strecke stehen bleibt.
curl https://meinpostify.de/api.php \
-H "Authorization: Bearer psty_your_api_key"
{
"ok": true,
"account": "you@example.com",
"balance": "18.00",
"currency": "EUR",
"letter_price": "2.00",
"letters_left": 9
}
Brief senden
POST https://meinpostify.de/api.php als multipart/form-data. Den Text in content senden oder eine fertige Datei als document anhängen. Eines von beiden muss dabei sein.
| Feld | Pflicht | Hinweis |
|---|---|---|
| recipient | ja | Name des Empfängers. Höchstens 200 Zeichen. |
| address | ja | Vollständige Postanschrift, eine Zeile je Adresszeile. Mindestens 8 Zeichen. |
| content | eines | Der Brieftext, bis 20000 Zeichen. |
| document | eines | PDF- oder Textdatei, bis 5 MB. |
| subject | nein | Betreffzeile über dem Text. Höchstens 200 Zeichen. |
| template | nein | Eines von classic, modern, minimal, statement. Standard ist die im Briefkopf gespeicherte Vorlage. |
| company | Briefkopf | Name im Briefkopf. Fällt auf den Briefkopf unter /letterhead.php zurück. |
| sender_address | Briefkopf | Rücksendezeile und Fußzeile, eine Zeile je Adresszeile. |
| sender_contact | Briefkopf | Telefon, E-Mail, Web, Registernummer. |
| signature | Briefkopf | Name unter dem Grußformel-Block. Ohne Angabe entfällt der Gruß. |
| info_rows | Briefkopf | JSON-Liste aus {label, value} für den Block rechts. {reference} und {date} werden je Brief gefüllt. |
| var[...] | wie deklariert | Je Variable aus dem Briefkopf ein Feld. Siehe Variablen weiter unten. |
| style | Briefkopf | JSON: family (serif, sans, times, helvetica, mono), body, head, leading, tracking. |
curl https://meinpostify.de/api.php \
-H "Authorization: Bearer psty_your_api_key" \
-F "recipient=Ilona Weber" \
-F $'address=Hauptstraße 3\n50667 Köln' \
-F "subject=Jahresabschluss 2025" \
-F "template=modern" \
-F "content=Sehr geehrte Frau Weber, anbei der Abschluss." \
-F "document=@statement.pdf"
{
"ok": true,
"letter_id": 41,
"reference": "PSTY-000041",
"recipient": "Ilona Weber",
"subject": "Jahresabschluss 2025",
"template": "modern",
"charged": "2.00",
"balance": "16.00",
"status": "sent"
}
Vorlagen
Vier Layouts, schwarz auf weiß. Die Kennung geht in template.
classic
Zentrierter Serifen-Briefkopf über doppelter Linie. Der Ton einer Kanzlei oder Steuerberatung.
modern
Monogramm links, Kontaktspalte rechts, eine Haarlinie. Der alltägliche Geschäftsbrief.
minimal
Kleine Wortmarke, weite Ränder, keine Linien. Leise und teuer, für Beratungen und Studios.
statement
Schwarzes Kopfband mit Monogramm, passendes Fußband. Wirkt wie ein Bescheid, nicht wie Post.
Variablen
Alles, was Sie im Informationsblock Ihres Briefkopfs in spitze Klammern setzen, etwa <kundennummer>, wird je Brief abgefragt. Die deklarierten Variablen stehen in der Antwort auf GET, eine Anbindung kann sie also auslesen statt zu raten.
{
"ok": true,
"template": "modern",
"variables": [
{ "key": "kundennummer", "name": "Kundennummer", "field": "var[kundennummer]" },
{ "key": "ihr_schreiben_vom", "name": "Ihr Schreiben vom", "field": "var[ihr_schreiben_vom]" }
]
}
curl https://meinpostify.de/api.php \
-H "Authorization: Bearer psty_your_api_key" \
-F "recipient=Ilona Weber" \
-F "address=Hauptstrasse 3, 50667 Koeln" \
-F "content=Dear Ms Weber, ..." \
-F "var[kundennummer]=4711" \
-F "var[ihr_schreiben_vom]=14 August 2026"
Ein JSON-Objekt in variables geht genauso, ebenso ein flaches Feld var_kundennummer. Eine weggelassene Variable druckt nichts, und eine Zeile, die nur aus dieser Variable bestand, entfällt auf dem Blatt statt leer zu erscheinen. Werte sind auf 120 Zeichen begrenzt.
Aus dem Browser
Für einen Test auf dem eigenen Rechner geht es direkt: der Endpunkt antwortet mit Access-Control-Allow-Origin: *, beantwortet den Preflight mit 204 und erlaubt die Header Authorization und Content-Type. Das gilt auch für eine lokale Datei über file://, deren Herkunft null lautet.
Zwei Stolpersteine: credentials: "include" verträgt sich nicht mit dem Stern und wird vom Browser abgelehnt, Cookies braucht die API ohnehin nicht. Und den Content-Type bei FormData nicht selbst setzen, sonst fehlt die multipart-Grenze.
const body = new FormData();
body.set('recipient', 'Ilona Weber');
body.set('address', 'Hauptstraße 3
50667 Köln');
body.set('subject', 'Jahresabschluss 2025');
body.set('content', 'Sehr geehrte Frau Weber, ...');
body.set('var[kundennummer]', '4711');
const response = await fetch('https://meinpostify.de/api.php', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
body,
});
const letter = await response.json();
const response = await fetch('https://meinpostify.de/api.php', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipient: 'Ilona Weber',
address: 'Hauptstraße 3
50667 Köln',
content: 'Sehr geehrte Frau Weber, ...',
var: { kundennummer: '4711' },
}),
});
Ein JSON-Körper wird gelesen wie Formulardaten. Eine Datei geht nur per multipart/form-data, ein Feld document im JSON wird ignoriert. Ungültiges JSON antwortet mit 400 invalid_json.
Für den echten Betrieb: ein Weiterleiter auf Ihrem Server, der den Schlüssel kennt und den Browser nie sehen lässt. Wichtig dabei, und die häufigste Stolperfalle: eine Seite mit fetch('send_letter.php') muss über einen Server laufen. Per Doppelklick geöffnet steht sie unter file://, dort führt niemand PHP aus, und der Aufruf endet im Verbindungsfehler. Zum Ausprobieren genügt im Ordner der Dateien: php -S localhost:8000, dann http://localhost:8000/index.html aufrufen.
<?php
// Der Schluessel liegt hier, nicht im Browser.
$apiKey = getenv('POSTIFY_KEY');
$curl = curl_init('https://meinpostify.de/api.php');
curl_setopt_array($curl, [
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => [
'recipient' => $_POST['recipient'] ?? '',
'address' => $_POST['address'] ?? '',
'subject' => $_POST['subject'] ?? '',
'content' => $_POST['content'] ?? '',
],
CURLOPT_RETURNTRANSFER => true,
]);
$answer = curl_exec($curl);
http_response_code(curl_getinfo($curl, CURLINFO_HTTP_CODE));
header('Content-Type: application/json');
echo $answer;
Das Druckblatt
Jeder Auftrag, aus der Übersicht wie über die API, erzeugt ein zweiseitiges PDF, das per Mail in die Druckerei geht. Seite 1 ist ein Geschäftsbrief nach DIN 5008 Form A: Briefkopf in der gewählten Vorlage, Rücksendezeile und Anschriftfeld passend für einen Fensterumschlag C5 oder C6, Informationsblock auf Höhe des Empfängers, Betreff, Text, Grußformel und eine Fußzeile mit Ihren vollständigen Absenderangaben. Die Maße stehen fest, unabhängig von Vorlage und Schrift: Anschriftfeld bei 45 mm, 85 mm breit, Informationsblock daneben, Betreff bei 98,46 mm, Faltmarken bei 105 mm und 210 mm. Seite 2 ist der Auftragsbeleg: Auftragsnummer, Vorlage, Typografie, beide Parteien, Betreff, Informationsblock, Variablen, Anlage, Eingangsweg, Konto und der berechnete Betrag.
Hängen Sie ein document an, reist es als zweiter Anhang mit, und Seite 1 vermerkt, dass die Anlage das zu druckende Blatt ist. Ihre hochgeladene Datei wird vom Server gelöscht, sobald die Mail heraus ist.
Statuscodes
| Code | Antwort | Bedeutung |
|---|---|---|
| 200 | Guthabenobjekt | Guthaben gelesen. |
| 201 | Briefobjekt | Berechnet, Druckauftrag ist heraus. |
| 202 | Briefobjekt, status failed | Berechnet und angenommen, aber die Meldung an die Druckerei kam nicht durch. Sie wird alle zehn Minuten wiederholt, bis sie durchgeht. Nicht erneut senden, der Brief wuerde sonst zweimal gedruckt. |
| 401 | missing_bearer_token, invalid_api_key | Kein Schlüssel oder ein Schlüssel, den es nicht mehr gibt. |
| 403 | email_unverified | Die E-Mail-Adresse des Kontos ist nicht bestätigt. Link im Postfach, oder /verify.php?neu=1 im angemeldeten Browser. |
| 402 | insufficient_balance | Guthaben unter 2,00 EUR. In der Übersicht aufladen. |
| 400 | invalid_json | Der Koerper war als JSON angekuendigt, ist aber keins. |
| 405 | method_not_allowed | Nur GET, POST und OPTIONS. |
| 422 | recipient_required, address_required, address_not_germany, address_zip_required, content_or_document_required, invalid_template, document_too_large, document_type | Ein Feld hat die Prüfung nicht bestanden. Die Meldung steht im Feld message. |
Wir liefern nur innerhalb Deutschlands ein. Eine Anschrift ohne fünfstellige Postleitzahl oder mit einem ausländischen Land wird mit 422 und dem Code address_not_germany beziehungsweise address_zip_required abgewiesen, bevor etwas berechnet wird.
Ein Aufruf der Schnittstelle ist zugleich das ausdrückliche Verlangen, sofort mit dem Druck zu beginnen; das Widerrufsrecht erlischt mit der Einlieferung (§ 356 Abs. 4 BGB). Im Browser bestätigt der Kunde das mit einem Haken, in der Schnittstelle steht es in den AGB.
Das Guthaben wird in einer einzigen atomaren Anweisung belastet. Zwei gleichzeitige Aufrufe gegen ein Guthaben von zwei Euro können daher nicht beide durchgehen, der zweite bekommt 402.
Beispiele
$curl = curl_init('https://meinpostify.de/api.php');
curl_setopt_array($curl, [
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => [
'company' => 'Alscher Steuerberatung',
'recipient' => 'Ilona Weber',
'address' => "Hauptstraße 3\n50667 Köln",
'template' => 'modern',
'content' => 'Sehr geehrte Frau Weber, anbei der Abschluss.',
],
CURLOPT_RETURNTRANSFER => true,
]);
$letter = json_decode(curl_exec($curl), true);
const body = new FormData();
body.set('recipient', 'Ilona Weber');
body.set('address', 'Hauptstraße 3, 50667 Köln');
body.set('template', 'statement');
body.set('document', new Blob([pdfBytes], { type: 'application/pdf' }), 'statement.pdf');
const response = await fetch('https://meinpostify.de/api.php', {
method: 'POST',
headers: { Authorization: `Bearer ${apiKey}` },
body,
});
const letter = await response.json();